resolv_conf/
lib.rs

1//! The crate simply parses `/etc/resolv.conf` file and creates a config object
2//!
3//! # Examples
4//!
5//! ## Parsing a config from a string
6//! ```rust
7//! extern crate resolv_conf;
8//!
9//! use std::net::{Ipv4Addr, Ipv6Addr};
10//! use resolv_conf::{ScopedIp, Config, Network};
11//!
12//! fn main() {
13//!     let config_str = "
14//! options ndots:8 timeout:8 attempts:8
15//!
16//! domain example.com
17//! search example.com sub.example.com
18//!
19//! nameserver 2001:4860:4860::8888
20//! nameserver 2001:4860:4860::8844
21//! nameserver 8.8.8.8
22//! nameserver 8.8.4.4
23//!
24//! options rotate
25//! options inet6 no-tld-query
26//!
27//! sortlist 130.155.160.0/255.255.240.0 130.155.0.0";
28//!
29//!     // Parse the config.
30//!     let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
31//!
32//!     // We can build configs manually as well, either directly or with Config::new()
33//!     let mut expected_config = Config::new();
34//!     expected_config.nameservers = vec![
35//!         ScopedIp::V6(Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888), None),
36//!         ScopedIp::V6(Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8844), None),
37//!         ScopedIp::V4(Ipv4Addr::new(8, 8, 8, 8)),
38//!         ScopedIp::V4(Ipv4Addr::new(8, 8, 4, 4)),
39//!     ];
40//!     expected_config.sortlist = vec![
41//!         Network::V4(Ipv4Addr::new(130, 155, 160, 0), Ipv4Addr::new(255, 255, 240, 0)),
42//!         Network::V4(Ipv4Addr::new(130, 155, 0, 0), Ipv4Addr::new(255, 255, 0, 0)),
43//!     ];
44//!     expected_config.debug = false;
45//!     expected_config.ndots = 8;
46//!     expected_config.timeout = 8;
47//!     expected_config.attempts = 8;
48//!     expected_config.rotate = true;
49//!     expected_config.no_check_names = false;
50//!     expected_config.inet6 = true;
51//!     expected_config.ip6_bytestring = false;
52//!     expected_config.ip6_dotint = false;
53//!     expected_config.edns0 = false;
54//!     expected_config.single_request = false;
55//!     expected_config.single_request_reopen = false;
56//!     expected_config.no_tld_query = true;
57//!     expected_config.use_vc = false;
58//!     expected_config.set_domain(String::from("example.com"));
59//!     expected_config.set_search(vec![
60//!         String::from("example.com"),
61//!         String::from("sub.example.com")
62//!     ]);
63//!
64//!     // We can compare configurations, since resolv_conf::Config implements Eq
65//!     assert_eq!(parsed_config, expected_config);
66//! }
67//! ```
68//!
69//! ## Parsing a file
70//!
71//! ```rust
72//! use std::io::Read;
73//! use std::fs::File;
74//!
75//! extern crate resolv_conf;
76//!
77//! fn main() {
78//!     // Read the file
79//!     let mut buf = Vec::with_capacity(4096);
80//!     let mut f = File::open("/etc/resolv.conf").unwrap();
81//!     f.read_to_end(&mut buf).unwrap();
82//!
83//!     // Parse the buffer
84//!     let cfg = resolv_conf::Config::parse(&buf).unwrap();
85//!
86//!     // Print the config
87//!     println!("---- Parsed /etc/resolv.conf -----\n{:#?}\n", cfg);
88//! }
89//! ```
90
91#![warn(missing_debug_implementations, missing_docs, unreachable_pub)]
92#![warn(clippy::use_self)]
93
94use std::fmt;
95use std::iter::Iterator;
96use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
97use std::slice::Iter;
98use std::str::{self, from_utf8, FromStr, Utf8Error};
99
100mod ip;
101pub use ip::{AddrParseError, Network, ScopedIp};
102
103/// Represent a resolver configuration, as described in `man 5 resolv.conf`.
104/// The options and defaults match those in the linux `man` page.
105///
106/// Note: while most fields in the structure are public the `search` and
107/// `domain` fields must be accessed via methods. This is because there are
108/// few different ways to treat `domain` field. In GNU libc `search` and
109/// `domain` replace each other ([`get_last_search_or_domain`]).
110/// In MacOS `/etc/resolve/*` files `domain` is treated in entirely different
111/// way.
112///
113/// Also consider using [`glibc_normalize`] and [`get_system_domain`] to match
114/// behavior of GNU libc.
115///
116/// ```rust
117/// extern crate resolv_conf;
118///
119/// use std::net::Ipv4Addr;
120/// use resolv_conf::{Config, ScopedIp};
121///
122/// fn main() {
123///     // Create a new config
124///     let mut config = Config::new();
125///     config.nameservers.push(ScopedIp::V4(Ipv4Addr::new(8, 8, 8, 8)));
126///     config.set_search(vec!["example.com".into()]);
127///
128///     // Parse a config
129///     let parsed = Config::parse("nameserver 8.8.8.8\nsearch example.com").unwrap();
130///     assert_eq!(parsed, config);
131/// }
132/// ```
133///
134/// [`glibc_normalize`]: #method.glibc_normalize
135/// [`get_last_search_or_domain`]: #method.get_last_search_or_domain
136/// [`get_system_domain`]: #method.get_system_domain
137#[derive(Clone, Debug, PartialEq, Eq)]
138pub struct Config {
139    /// List of nameservers
140    pub nameservers: Vec<ScopedIp>,
141    /// Indicated whether the last line that has been parsed is a "domain" directive or a "search"
142    /// directive. This is important for compatibility with glibc, since in glibc's implementation,
143    /// "search" and "domain" are mutually exclusive, and only the last directive is taken into
144    /// consideration.
145    last_search: LastSearch,
146    /// Domain to append to name when it doesn't contain ndots
147    domain: Option<String>,
148    /// List of suffixes to append to name when it doesn't contain ndots
149    search: Option<Vec<String>>,
150    /// List of preferred addresses
151    pub sortlist: Vec<Network>,
152    /// Enable DNS resolve debugging
153    pub debug: bool,
154    /// Number of dots in name to try absolute resolving first (default 1)
155    pub ndots: u32,
156    /// Dns query timeout (default 5 [sec])
157    pub timeout: u32,
158    /// Number of attempts to resolve name if server is inaccesible (default 2)
159    pub attempts: u32,
160    /// Round-robin selection of servers (default false)
161    pub rotate: bool,
162    /// Don't check names for validity (default false)
163    pub no_check_names: bool,
164    /// Try AAAA query before A
165    pub inet6: bool,
166    /// Use reverse lookup of ipv6 using bit-label format described instead
167    /// of nibble format
168    pub ip6_bytestring: bool,
169    /// Do ipv6 reverse lookups in ip6.int zone instead of ip6.arpa
170    /// (default false)
171    pub ip6_dotint: bool,
172    /// Enable dns extensions described in RFC 2671
173    pub edns0: bool,
174    /// Don't make ipv4 and ipv6 requests simultaneously
175    pub single_request: bool,
176    /// Use same socket for the A and AAAA requests
177    pub single_request_reopen: bool,
178    /// Don't resolve unqualified name as top level domain
179    pub no_tld_query: bool,
180    /// Force using TCP for DNS resolution
181    pub use_vc: bool,
182    /// Disable the automatic reloading of a changed configuration file
183    pub no_reload: bool,
184    /// Optionally send the AD (authenticated data) bit in queries
185    pub trust_ad: bool,
186    /// The order in which databases should be searched during a lookup
187    /// **(openbsd-only)**
188    pub lookup: Vec<Lookup>,
189    /// The order in which internet protocol families should be prefered
190    /// **(openbsd-only)**
191    pub family: Vec<Family>,
192    /// Suppress AAAA queries made by the stub resolver
193    pub no_aaaa: bool,
194}
195
196impl Config {
197    /// Create a new `Config` object with default values.
198    ///
199    /// ```rust
200    /// # extern crate resolv_conf;
201    /// use resolv_conf::Config;
202    /// # fn main() {
203    /// let config = Config::new();
204    /// assert_eq!(config.nameservers, vec![]);
205    /// assert!(config.get_domain().is_none());
206    /// assert!(config.get_search().is_none());
207    /// assert_eq!(config.sortlist, vec![]);
208    /// assert_eq!(config.debug, false);
209    /// assert_eq!(config.ndots, 1);
210    /// assert_eq!(config.timeout, 5);
211    /// assert_eq!(config.attempts, 2);
212    /// assert_eq!(config.rotate, false);
213    /// assert_eq!(config.no_check_names, false);
214    /// assert_eq!(config.inet6, false);
215    /// assert_eq!(config.ip6_bytestring, false);
216    /// assert_eq!(config.ip6_dotint, false);
217    /// assert_eq!(config.edns0, false);
218    /// assert_eq!(config.single_request, false);
219    /// assert_eq!(config.single_request_reopen, false);
220    /// assert_eq!(config.no_tld_query, false);
221    /// assert_eq!(config.use_vc, false);
222    /// # }
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    /// Parse a buffer and return the corresponding `Config` object.
228    ///
229    /// ```rust
230    /// # extern crate resolv_conf;
231    /// use resolv_conf::{ScopedIp, Config};
232    /// # fn main() {
233    /// let config_str = "# /etc/resolv.conf
234    /// nameserver  8.8.8.8
235    /// nameserver  8.8.4.4
236    /// search      example.com sub.example.com
237    /// options     ndots:8 attempts:8";
238    ///
239    /// // Parse the config
240    /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
241    ///
242    /// // Print the config
243    /// println!("{:?}", parsed_config);
244    /// # }
245    /// ```
246    pub fn parse<T: AsRef<[u8]>>(buf: T) -> Result<Self, ParseError> {
247        let (new, mut errors) = Self::parse_with_errors(buf.as_ref());
248        let mut iter = errors.drain(..);
249        match iter.next() {
250            Some(err) => Err(err),
251            None => Ok(new),
252        }
253    }
254
255    /// Parse a buffer and return a best-effort parsed `Config` object along with any errors.
256    pub fn parse_with_errors(bytes: &[u8]) -> (Self, Vec<ParseError>) {
257        use ParseError::*;
258        let mut cfg = Self::new();
259        let mut errors = Vec::new();
260        'lines: for (lineno, line) in bytes.split(|&x| x == b'\n').enumerate() {
261            for &c in line.iter() {
262                if c != b'\t' && c != b' ' {
263                    if c == b';' || c == b'#' {
264                        continue 'lines;
265                    } else {
266                        break;
267                    }
268                }
269            }
270
271            // All that dances above to allow invalid utf-8 inside the comments
272            let str = match from_utf8(line) {
273                Ok(str) => str,
274                Err(e) => {
275                    errors.push(InvalidUtf8(lineno, e));
276                    continue;
277                }
278            };
279
280            // ignore everything after ';' or '#'
281            let text = match str.split([';', '#']).next() {
282                Some(text) => text,
283                None => {
284                    errors.push(InvalidValue(lineno));
285                    continue;
286                }
287            };
288
289            let mut words = text.split_whitespace();
290            let keyword = match words.next() {
291                Some(x) => x,
292                None => continue,
293            };
294
295            match keyword {
296                "nameserver" => {
297                    let srv = match words.next() {
298                        Some(srv) => srv,
299                        None => {
300                            errors.push(InvalidValue(lineno));
301                            continue;
302                        }
303                    };
304
305                    match ScopedIp::from_str(srv) {
306                        Ok(addr) => cfg.nameservers.push(addr),
307                        Err(e) => errors.push(InvalidIp(lineno, e)),
308                    }
309
310                    if words.next().is_some() {
311                        errors.push(ExtraData(lineno));
312                    }
313                }
314                "domain" => {
315                    let domain = match words.next() {
316                        Some(domain) => domain,
317                        None => {
318                            errors.push(InvalidValue(lineno));
319                            continue;
320                        }
321                    };
322
323                    cfg.set_domain(domain.to_owned());
324                    if words.next().is_some() {
325                        errors.push(ExtraData(lineno));
326                    }
327                }
328                "search" => {
329                    let mut search = Vec::new();
330                    for word in words {
331                        if !word.contains(',') {
332                            // Easy to get confused between spaces and commas here
333                            // https://github.com/hickory-dns/resolv-conf/issues/58
334                            search.push(word.to_owned());
335                        }
336                    }
337                    cfg.set_search(search);
338                }
339                "sortlist" => {
340                    cfg.sortlist.clear();
341                    for pair in words {
342                        match Network::from_str(pair) {
343                            Ok(network) => cfg.sortlist.push(network),
344                            Err(e) => errors.push(InvalidIp(lineno, e)),
345                        }
346                    }
347                }
348                "options" => {
349                    for pair in words {
350                        let mut iter = pair.splitn(2, ':');
351                        let key = match iter.next() {
352                            Some(key) => key,
353                            None => {
354                                errors.push(InvalidValue(lineno));
355                                continue 'lines;
356                            }
357                        };
358
359                        let value = iter.next();
360                        if iter.next().is_some() {
361                            errors.push(ExtraData(lineno));
362                            continue 'lines;
363                        }
364
365                        match (key, value) {
366                            // TODO(tailhook) ensure that values are None?
367                            ("debug", _) => cfg.debug = true,
368                            ("ndots", Some(x)) => match u32::from_str(x) {
369                                Ok(ndots) => cfg.ndots = ndots,
370                                Err(_) => errors.push(InvalidOptionValue(lineno)),
371                            },
372                            ("timeout", Some(x)) => match u32::from_str(x) {
373                                Ok(timeout) => cfg.timeout = timeout,
374                                Err(_) => errors.push(InvalidOptionValue(lineno)),
375                            },
376                            ("attempts", Some(x)) => match u32::from_str(x) {
377                                Ok(attempts) => cfg.attempts = attempts,
378                                Err(_) => errors.push(InvalidOptionValue(lineno)),
379                            },
380                            ("rotate", _) => cfg.rotate = true,
381                            ("no-check-names", _) => cfg.no_check_names = true,
382                            ("inet6", _) => cfg.inet6 = true,
383                            ("ip6-bytestring", _) => cfg.ip6_bytestring = true,
384                            ("ip6-dotint", _) => cfg.ip6_dotint = true,
385                            ("no-ip6-dotint", _) => cfg.ip6_dotint = false,
386                            ("edns0", _) => cfg.edns0 = true,
387                            ("single-request", _) => cfg.single_request = true,
388                            ("single-request-reopen", _) => cfg.single_request_reopen = true,
389                            ("no-reload", _) => cfg.no_reload = true,
390                            ("trust-ad", _) => cfg.trust_ad = true,
391                            ("no-tld-query", _) => cfg.no_tld_query = true,
392                            ("use-vc", _) => cfg.use_vc = true,
393                            ("no-aaaa", _) => cfg.no_aaaa = true,
394                            _ => errors.push(InvalidOption(lineno)),
395                        }
396                    }
397                }
398                "lookup" => {
399                    for word in words {
400                        match word {
401                            "file" => cfg.lookup.push(Lookup::File),
402                            "bind" => cfg.lookup.push(Lookup::Bind),
403                            extra => cfg.lookup.push(Lookup::Extra(extra.to_string())),
404                        }
405                    }
406                }
407                "family" => {
408                    for word in words {
409                        match word {
410                            "inet4" => cfg.family.push(Family::Inet4),
411                            "inet6" => cfg.family.push(Family::Inet6),
412                            _ => errors.push(InvalidValue(lineno)),
413                        }
414                    }
415                }
416                _ => errors.push(InvalidDirective(lineno)),
417            }
418        }
419
420        (cfg, errors)
421    }
422
423    /// Return the suffixes declared in the last "domain" or "search" directive.
424    ///
425    /// ```rust
426    /// # extern crate resolv_conf;
427    /// use resolv_conf::{ScopedIp, Config};
428    /// # fn main() {
429    /// let config_str = "search example.com sub.example.com\ndomain localdomain";
430    /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
431    /// let domains = parsed_config.get_last_search_or_domain()
432    ///                            .map(|domain| domain.clone())
433    ///                            .collect::<Vec<String>>();
434    /// assert_eq!(domains, vec![String::from("localdomain")]);
435    ///
436    /// let config_str = "domain localdomain\nsearch example.com sub.example.com";
437    /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
438    /// let domains = parsed_config.get_last_search_or_domain()
439    ///                            .map(|domain| domain.clone())
440    ///                            .collect::<Vec<String>>();
441    /// assert_eq!(domains, vec![String::from("example.com"), String::from("sub.example.com")]);
442    /// # }
443    pub fn get_last_search_or_domain(&self) -> DomainIter<'_> {
444        let domain_iter = match self.last_search {
445            LastSearch::Search => {
446                DomainIterInternal::Search(self.get_search().map(|domains| domains.iter()))
447            }
448            LastSearch::Domain => DomainIterInternal::Domain(self.get_domain()),
449            LastSearch::None => DomainIterInternal::None,
450        };
451        DomainIter(domain_iter)
452    }
453
454    /// Return the domain declared in the last "domain" directive.
455    pub fn get_domain(&self) -> Option<&String> {
456        self.domain.as_ref()
457    }
458
459    /// Return the domains declared in the last "search" directive.
460    pub fn get_search(&self) -> Option<&Vec<String>> {
461        self.search.as_ref()
462    }
463
464    /// Set the domain corresponding to the "domain" directive.
465    pub fn set_domain(&mut self, domain: String) {
466        self.domain = Some(domain);
467        self.last_search = LastSearch::Domain;
468    }
469
470    /// Set the domains corresponding the "search" directive.
471    pub fn set_search(&mut self, search: Vec<String>) {
472        self.search = Some(search);
473        self.last_search = LastSearch::Search;
474    }
475
476    /// Normalize config according to glibc rulees
477    ///
478    /// Currently this method does the following things:
479    ///
480    /// 1. Truncates list of nameservers to 3 at max
481    /// 2. Truncates search list to 6 at max
482    ///
483    /// Other normalizations may be added in future as long as they hold true
484    /// for a particular GNU libc implementation.
485    ///
486    /// Note: this method is not called after parsing, because we think it's
487    /// not forward-compatible to rely on such small and ugly limits. Still,
488    /// it's useful to keep implementation as close to glibc as possible.
489    pub fn glibc_normalize(&mut self) {
490        self.nameservers.truncate(NAMESERVER_LIMIT);
491        self.search = self.search.take().map(|mut s| {
492            s.truncate(SEARCH_LIMIT);
493            s
494        });
495    }
496
497    /// Get nameserver or on the local machine
498    pub fn get_nameservers_or_local(&self) -> Vec<ScopedIp> {
499        if self.nameservers.is_empty() {
500            vec![
501                ScopedIp::from(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
502                ScopedIp::from(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))),
503            ]
504        } else {
505            self.nameservers.to_vec()
506        }
507    }
508
509    /// Get domain from config or fallback to the suffix of a hostname
510    ///
511    /// This is how glibc finds out a hostname.
512    pub fn get_system_domain(&self) -> Option<String> {
513        if self.domain.is_some() {
514            return self.domain.clone();
515        }
516
517        // This buffer is far larger than what most systems will ever allow, eg.
518        // linux uses 64 via _SC_HOST_NAME_MAX even though POSIX says the size
519        // must be _at least_ _POSIX_HOST_NAME_MAX (255), but other systems can
520        // be larger, so we just use a sufficiently sized buffer so we can defer
521        // a heap allocation until the last possible moment.
522        let mut hostname = [0u8; 1024];
523
524        #[cfg(all(target_os = "linux", target_feature = "crt-static"))]
525        {
526            use std::{fs::File, io::Read};
527            let mut file = File::open("/proc/sys/kernel/hostname").ok()?;
528            let read_bytes = file.read(&mut hostname).ok()?;
529
530            // According to Linux kernel's proc_dostring handler, user-space reads
531            // of /proc/sys entries which have a string value are terminated by
532            // a newline character. While libc gethostname() terminates the hostname
533            // with a null character. Hence, to match the behavior of gethostname()
534            // it is necessary to replace the newline with a null character.
535            if read_bytes == hostname.len() && hostname[read_bytes - 1] != b'\n' {
536                // In this case the string read from /proc/sys/kernel/hostname is
537                // truncated and cannot be terminated by a null character
538                return None;
539            }
540            // Since any non-truncated string read from /proc/sys/kernel/hostname
541            // ends with a newline character, read_bytes > 0.
542            hostname[read_bytes - 1] = 0;
543        }
544
545        #[cfg(not(all(target_os = "linux", target_feature = "crt-static")))]
546        {
547            /*unsafe*/
548            extern "C" {
549                fn gethostname(hostname: *mut u8, size: usize) -> i32;
550            }
551
552            unsafe {
553                if gethostname(hostname.as_mut_ptr(), hostname.len()) < 0 {
554                    return None;
555                }
556            }
557        }
558
559        domain_from_host(&hostname).map(|s| s.to_owned())
560    }
561}
562
563impl Default for Config {
564    fn default() -> Self {
565        Self {
566            nameservers: Vec::new(),
567            domain: None,
568            search: None,
569            last_search: LastSearch::None,
570            sortlist: Vec::new(),
571            debug: false,
572            ndots: 1,
573            timeout: 5,
574            attempts: 2,
575            rotate: false,
576            no_check_names: false,
577            inet6: false,
578            ip6_bytestring: false,
579            ip6_dotint: false,
580            edns0: false,
581            single_request: false,
582            single_request_reopen: false,
583            no_tld_query: false,
584            use_vc: false,
585            no_reload: false,
586            trust_ad: false,
587            lookup: Vec::new(),
588            family: Vec::new(),
589            no_aaaa: false,
590        }
591    }
592}
593
594impl fmt::Display for Config {
595    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
596        let Self {
597            nameservers,
598            last_search,
599            domain,
600            search,
601            sortlist,
602            debug,
603            ndots,
604            timeout,
605            attempts,
606            rotate,
607            no_check_names,
608            inet6,
609            ip6_bytestring,
610            ip6_dotint,
611            edns0,
612            single_request,
613            single_request_reopen,
614            no_tld_query,
615            use_vc,
616            no_reload,
617            trust_ad,
618            lookup,
619            family,
620            no_aaaa,
621        } = self;
622
623        for nameserver in nameservers.iter() {
624            writeln!(fmt, "nameserver {nameserver}")?;
625        }
626
627        if last_search != &LastSearch::Domain {
628            if let Some(domain) = domain {
629                writeln!(fmt, "domain {domain}")?;
630            }
631        }
632
633        if let Some(search) = search {
634            if !search.is_empty() {
635                write!(fmt, "search")?;
636                for suffix in search.iter() {
637                    write!(fmt, " {suffix}")?;
638                }
639                writeln!(fmt)?;
640            }
641        }
642
643        if last_search == &LastSearch::Domain {
644            if let Some(domain) = &self.domain {
645                writeln!(fmt, "domain {domain}")?;
646            }
647        }
648
649        if !sortlist.is_empty() {
650            write!(fmt, "sortlist")?;
651            for network in sortlist.iter() {
652                write!(fmt, " {network}")?;
653            }
654            writeln!(fmt)?;
655        }
656
657        if !lookup.is_empty() {
658            write!(fmt, "lookup")?;
659            for db in lookup.iter() {
660                match db {
661                    Lookup::File => write!(fmt, " file")?,
662                    Lookup::Bind => write!(fmt, " bind")?,
663                    Lookup::Extra(extra) => write!(fmt, " {extra}")?,
664                }
665            }
666            writeln!(fmt)?;
667        }
668
669        if !family.is_empty() {
670            write!(fmt, "family")?;
671            for fam in family.iter() {
672                match fam {
673                    Family::Inet4 => write!(fmt, " inet4")?,
674                    Family::Inet6 => write!(fmt, " inet6")?,
675                }
676            }
677            writeln!(fmt)?;
678        }
679
680        if *debug {
681            writeln!(fmt, "options debug")?;
682        }
683        if *ndots != 1 {
684            writeln!(fmt, "options ndots:{}", self.ndots)?;
685        }
686        if *timeout != 5 {
687            writeln!(fmt, "options timeout:{}", self.timeout)?;
688        }
689        if *attempts != 2 {
690            writeln!(fmt, "options attempts:{}", self.attempts)?;
691        }
692        if *rotate {
693            writeln!(fmt, "options rotate")?;
694        }
695        if *no_check_names {
696            writeln!(fmt, "options no-check-names")?;
697        }
698        if *inet6 {
699            writeln!(fmt, "options inet6")?;
700        }
701        if *ip6_bytestring {
702            writeln!(fmt, "options ip6-bytestring")?;
703        }
704        if *ip6_dotint {
705            writeln!(fmt, "options ip6-dotint")?;
706        }
707        if *edns0 {
708            writeln!(fmt, "options edns0")?;
709        }
710        if *single_request {
711            writeln!(fmt, "options single-request")?;
712        }
713        if *single_request_reopen {
714            writeln!(fmt, "options single-request-reopen")?;
715        }
716        if *no_tld_query {
717            writeln!(fmt, "options no-tld-query")?;
718        }
719        if *use_vc {
720            writeln!(fmt, "options use-vc")?;
721        }
722        if *no_reload {
723            writeln!(fmt, "options no-reload")?;
724        }
725        if *trust_ad {
726            writeln!(fmt, "options trust-ad")?;
727        }
728        if *no_aaaa {
729            writeln!(fmt, "options no-aaaa")?;
730        }
731
732        Ok(())
733    }
734}
735
736/// Error while parsing resolv.conf file
737#[derive(Debug)]
738pub enum ParseError {
739    /// Error that may be returned when the string to parse contains invalid UTF-8 sequences
740    InvalidUtf8(usize, Utf8Error),
741    /// Error returned a value for a given directive is invalid.
742    /// This can also happen when the value is missing, if the directive requires a value.
743    InvalidValue(usize),
744    /// Error returned when a value for a given option is invalid.
745    /// This can also happen when the value is missing, if the option requires a value.
746    InvalidOptionValue(usize),
747    /// Error returned when a invalid option is found.
748    InvalidOption(usize),
749    /// Error returned when a invalid directive is found.
750    InvalidDirective(usize),
751    /// Error returned when a value cannot be parsed an an IP address.
752    InvalidIp(usize, AddrParseError),
753    /// Error returned when there is extra data at the end of a line.
754    ExtraData(usize),
755}
756
757impl std::fmt::Display for ParseError {
758    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
759        match self {
760            Self::InvalidUtf8(line, err) => write!(f, "bad unicode at line {line}: {err}"),
761            Self::InvalidValue(line) => write!(
762                f,
763                "directive at line {line} is improperly formatted or contains invalid value",
764            ),
765            Self::InvalidOptionValue(line) => write!(
766                f,
767                "directive options at line {line} contains invalid value of some option",
768            ),
769            Self::InvalidOption(line) => {
770                write!(f, "option at line {line} is not recognized")
771            }
772            Self::InvalidDirective(line) => {
773                write!(f, "directive at line {line} is not recognized")
774            }
775            Self::InvalidIp(line, err) => {
776                write!(f, "directive at line {line} contains invalid IP: {err}")
777            }
778            Self::ExtraData(line) => write!(f, "extra data at the end of line {line}"),
779        }
780    }
781}
782
783impl std::error::Error for ParseError {
784    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
785        match self {
786            Self::InvalidUtf8(_, err) => Some(err),
787            _ => None,
788        }
789    }
790}
791
792/// An iterator returned by [`Config.get_last_search_or_domain`](struct.Config.html#method.get_last_search_or_domain)
793#[derive(Debug, Clone)]
794pub struct DomainIter<'a>(DomainIterInternal<'a>);
795
796impl<'a> Iterator for DomainIter<'a> {
797    type Item = &'a String;
798
799    fn next(&mut self) -> Option<Self::Item> {
800        self.0.next()
801    }
802}
803
804#[derive(Debug, Clone)]
805enum DomainIterInternal<'a> {
806    Search(Option<Iter<'a, String>>),
807    Domain(Option<&'a String>),
808    None,
809}
810
811impl<'a> Iterator for DomainIterInternal<'a> {
812    type Item = &'a String;
813
814    fn next(&mut self) -> Option<Self::Item> {
815        match self {
816            DomainIterInternal::Search(Some(domains)) => domains.next(),
817            DomainIterInternal::Domain(domain) => domain.take(),
818            _ => None,
819        }
820    }
821}
822
823/// The databases that should be searched during a lookup.
824/// This option is commonly found on openbsd.
825#[derive(Clone, Debug, PartialEq, Eq)]
826pub enum Lookup {
827    /// Search for entries in /etc/hosts
828    File,
829    /// Query a domain name server
830    Bind,
831    /// A database we don't know yet
832    Extra(String),
833}
834
835/// The internet protocol family that is prefered.
836/// This option is commonly found on openbsd.
837#[derive(Clone, Debug, PartialEq, Eq)]
838pub enum Family {
839    /// A A lookup for an ipv4 address
840    Inet4,
841    /// A AAAA lookup for an ipv6 address
842    Inet6,
843}
844
845/// Parses the domain name from a hostname, if available
846fn domain_from_host(hostname: &[u8]) -> Option<&str> {
847    let mut start = None;
848    for (i, b) in hostname.iter().copied().enumerate() {
849        if b == b'.' && start.is_none() {
850            start = Some(i);
851            continue;
852        } else if b > 0 {
853            continue;
854        }
855
856        return match start? {
857            // Avoid empty domains
858            start if i - start < 2 => None,
859            start => str::from_utf8(&hostname[start + 1..i]).ok(),
860        };
861    }
862
863    None
864}
865
866#[derive(Copy, Clone, PartialEq, Eq, Debug)]
867enum LastSearch {
868    None,
869    Domain,
870    Search,
871}
872
873const NAMESERVER_LIMIT: usize = 3;
874const SEARCH_LIMIT: usize = 6;
875
876#[cfg(test)]
877mod test {
878    use super::domain_from_host;
879    use super::Config;
880
881    #[test]
882    fn parses_domain_name() {
883        assert!(domain_from_host(b"regular-hostname\0").is_none());
884
885        assert_eq!(domain_from_host(b"with.domain-name\0"), Some("domain-name"));
886        assert_eq!(
887            domain_from_host(b"with.multiple.dots\0"),
888            Some("multiple.dots")
889        );
890
891        assert!(domain_from_host(b"hostname.\0").is_none());
892        assert_eq!(domain_from_host(b"host.a\0"), Some("a"));
893        assert_eq!(domain_from_host(b"host.au\0"), Some("au"));
894    }
895
896    #[test]
897    fn comma_search() {
898        let config = Config::parse(COMMA_SEARCH).unwrap();
899        assert_eq!(
900            config.get_search().unwrap(),
901            &["another.example.com".to_string(),]
902        );
903    }
904
905    const COMMA_SEARCH: &str = r#"search example.com,sub.example.com another.example.com"#;
906}