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        Self::from_slice(buf.as_ref())
248    }
249
250    fn from_slice(bytes: &[u8]) -> Result<Self, ParseError> {
251        use ParseError::*;
252        let mut cfg = Self::new();
253        'lines: for (lineno, line) in bytes.split(|&x| x == b'\n').enumerate() {
254            for &c in line.iter() {
255                if c != b'\t' && c != b' ' {
256                    if c == b';' || c == b'#' {
257                        continue 'lines;
258                    } else {
259                        break;
260                    }
261                }
262            }
263            // All that dances above to allow invalid utf-8 inside the comments
264            let mut words = from_utf8(line)
265            .map_err(|e| InvalidUtf8(lineno, e))?
266            // ignore everything after ';' or '#'
267            .split([';', '#'])
268            .next()
269            .ok_or(InvalidValue(lineno))?
270            .split_whitespace();
271            let keyword = match words.next() {
272                Some(x) => x,
273                None => continue,
274            };
275            match keyword {
276                "nameserver" => {
277                    let srv = words
278                        .next()
279                        .ok_or(InvalidValue(lineno))
280                        .map(|addr| addr.parse().map_err(|e| InvalidIp(lineno, e)))??;
281                    cfg.nameservers.push(srv);
282                    if words.next().is_some() {
283                        return Err(ExtraData(lineno));
284                    }
285                }
286                "domain" => {
287                    let dom = words
288                        .next()
289                        .and_then(|x| x.parse().ok())
290                        .ok_or(InvalidValue(lineno))?;
291                    cfg.set_domain(dom);
292                    if words.next().is_some() {
293                        return Err(ExtraData(lineno));
294                    }
295                }
296                "search" => {
297                    cfg.set_search(words.map(|x| x.to_string()).collect());
298                }
299                "sortlist" => {
300                    cfg.sortlist.clear();
301                    for pair in words {
302                        cfg.sortlist
303                            .push(Network::from_str(pair).map_err(|e| InvalidIp(lineno, e))?);
304                    }
305                }
306                "options" => {
307                    for pair in words {
308                        let mut iter = pair.splitn(2, ':');
309                        let key = iter.next().unwrap();
310                        let value = iter.next();
311                        if iter.next().is_some() {
312                            return Err(ExtraData(lineno));
313                        }
314                        match (key, value) {
315                            // TODO(tailhook) ensure that values are None?
316                            ("debug", _) => cfg.debug = true,
317                            ("ndots", Some(x)) => {
318                                cfg.ndots = x.parse().map_err(|_| InvalidOptionValue(lineno))?
319                            }
320                            ("timeout", Some(x)) => {
321                                cfg.timeout = x.parse().map_err(|_| InvalidOptionValue(lineno))?
322                            }
323                            ("attempts", Some(x)) => {
324                                cfg.attempts = x.parse().map_err(|_| InvalidOptionValue(lineno))?
325                            }
326                            ("rotate", _) => cfg.rotate = true,
327                            ("no-check-names", _) => cfg.no_check_names = true,
328                            ("inet6", _) => cfg.inet6 = true,
329                            ("ip6-bytestring", _) => cfg.ip6_bytestring = true,
330                            ("ip6-dotint", _) => cfg.ip6_dotint = true,
331                            ("no-ip6-dotint", _) => cfg.ip6_dotint = false,
332                            ("edns0", _) => cfg.edns0 = true,
333                            ("single-request", _) => cfg.single_request = true,
334                            ("single-request-reopen", _) => cfg.single_request_reopen = true,
335                            ("no-reload", _) => cfg.no_reload = true,
336                            ("trust-ad", _) => cfg.trust_ad = true,
337                            ("no-tld-query", _) => cfg.no_tld_query = true,
338                            ("use-vc", _) => cfg.use_vc = true,
339                            ("no-aaaa", _) => cfg.no_aaaa = true,
340                            _ => return Err(InvalidOption(lineno)),
341                        }
342                    }
343                }
344                "lookup" => {
345                    for word in words {
346                        match word {
347                            "file" => cfg.lookup.push(Lookup::File),
348                            "bind" => cfg.lookup.push(Lookup::Bind),
349                            extra => cfg.lookup.push(Lookup::Extra(extra.to_string())),
350                        }
351                    }
352                }
353                "family" => {
354                    for word in words {
355                        match word {
356                            "inet4" => cfg.family.push(Family::Inet4),
357                            "inet6" => cfg.family.push(Family::Inet6),
358                            _ => return Err(InvalidValue(lineno)),
359                        }
360                    }
361                }
362                _ => return Err(InvalidDirective(lineno)),
363            }
364        }
365        Ok(cfg)
366    }
367
368    /// Return the suffixes declared in the last "domain" or "search" directive.
369    ///
370    /// ```rust
371    /// # extern crate resolv_conf;
372    /// use resolv_conf::{ScopedIp, Config};
373    /// # fn main() {
374    /// let config_str = "search example.com sub.example.com\ndomain localdomain";
375    /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
376    /// let domains = parsed_config.get_last_search_or_domain()
377    ///                            .map(|domain| domain.clone())
378    ///                            .collect::<Vec<String>>();
379    /// assert_eq!(domains, vec![String::from("localdomain")]);
380    ///
381    /// let config_str = "domain localdomain\nsearch example.com sub.example.com";
382    /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
383    /// let domains = parsed_config.get_last_search_or_domain()
384    ///                            .map(|domain| domain.clone())
385    ///                            .collect::<Vec<String>>();
386    /// assert_eq!(domains, vec![String::from("example.com"), String::from("sub.example.com")]);
387    /// # }
388    pub fn get_last_search_or_domain(&self) -> DomainIter<'_> {
389        let domain_iter = match self.last_search {
390            LastSearch::Search => {
391                DomainIterInternal::Search(self.get_search().map(|domains| domains.iter()))
392            }
393            LastSearch::Domain => DomainIterInternal::Domain(self.get_domain()),
394            LastSearch::None => DomainIterInternal::None,
395        };
396        DomainIter(domain_iter)
397    }
398
399    /// Return the domain declared in the last "domain" directive.
400    pub fn get_domain(&self) -> Option<&String> {
401        self.domain.as_ref()
402    }
403
404    /// Return the domains declared in the last "search" directive.
405    pub fn get_search(&self) -> Option<&Vec<String>> {
406        self.search.as_ref()
407    }
408
409    /// Set the domain corresponding to the "domain" directive.
410    pub fn set_domain(&mut self, domain: String) {
411        self.domain = Some(domain);
412        self.last_search = LastSearch::Domain;
413    }
414
415    /// Set the domains corresponding the "search" directive.
416    pub fn set_search(&mut self, search: Vec<String>) {
417        self.search = Some(search);
418        self.last_search = LastSearch::Search;
419    }
420
421    /// Normalize config according to glibc rulees
422    ///
423    /// Currently this method does the following things:
424    ///
425    /// 1. Truncates list of nameservers to 3 at max
426    /// 2. Truncates search list to 6 at max
427    ///
428    /// Other normalizations may be added in future as long as they hold true
429    /// for a particular GNU libc implementation.
430    ///
431    /// Note: this method is not called after parsing, because we think it's
432    /// not forward-compatible to rely on such small and ugly limits. Still,
433    /// it's useful to keep implementation as close to glibc as possible.
434    pub fn glibc_normalize(&mut self) {
435        self.nameservers.truncate(NAMESERVER_LIMIT);
436        self.search = self.search.take().map(|mut s| {
437            s.truncate(SEARCH_LIMIT);
438            s
439        });
440    }
441
442    /// Get nameserver or on the local machine
443    pub fn get_nameservers_or_local(&self) -> Vec<ScopedIp> {
444        if self.nameservers.is_empty() {
445            vec![
446                ScopedIp::from(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
447                ScopedIp::from(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))),
448            ]
449        } else {
450            self.nameservers.to_vec()
451        }
452    }
453
454    /// Get domain from config or fallback to the suffix of a hostname
455    ///
456    /// This is how glibc finds out a hostname.
457    pub fn get_system_domain(&self) -> Option<String> {
458        if self.domain.is_some() {
459            return self.domain.clone();
460        }
461
462        // This buffer is far larger than what most systems will ever allow, eg.
463        // linux uses 64 via _SC_HOST_NAME_MAX even though POSIX says the size
464        // must be _at least_ _POSIX_HOST_NAME_MAX (255), but other systems can
465        // be larger, so we just use a sufficiently sized buffer so we can defer
466        // a heap allocation until the last possible moment.
467        let mut hostname = [0u8; 1024];
468
469        #[cfg(all(target_os = "linux", target_feature = "crt-static"))]
470        {
471            use std::{fs::File, io::Read};
472            let mut file = File::open("/proc/sys/kernel/hostname").ok()?;
473            let read_bytes = file.read(&mut hostname).ok()?;
474
475            // According to Linux kernel's proc_dostring handler, user-space reads
476            // of /proc/sys entries which have a string value are terminated by
477            // a newline character. While libc gethostname() terminates the hostname
478            // with a null character. Hence, to match the behavior of gethostname()
479            // it is necessary to replace the newline with a null character.
480            if read_bytes == hostname.len() && hostname[read_bytes - 1] != b'\n' {
481                // In this case the string read from /proc/sys/kernel/hostname is
482                // truncated and cannot be terminated by a null character
483                return None;
484            }
485            // Since any non-truncated string read from /proc/sys/kernel/hostname
486            // ends with a newline character, read_bytes > 0.
487            hostname[read_bytes - 1] = 0;
488        }
489
490        #[cfg(not(all(target_os = "linux", target_feature = "crt-static")))]
491        {
492            /*unsafe*/
493            extern "C" {
494                fn gethostname(hostname: *mut u8, size: usize) -> i32;
495            }
496
497            unsafe {
498                if gethostname(hostname.as_mut_ptr(), hostname.len()) < 0 {
499                    return None;
500                }
501            }
502        }
503
504        domain_from_host(&hostname).map(|s| s.to_owned())
505    }
506}
507
508impl Default for Config {
509    fn default() -> Self {
510        Self {
511            nameservers: Vec::new(),
512            domain: None,
513            search: None,
514            last_search: LastSearch::None,
515            sortlist: Vec::new(),
516            debug: false,
517            ndots: 1,
518            timeout: 5,
519            attempts: 2,
520            rotate: false,
521            no_check_names: false,
522            inet6: false,
523            ip6_bytestring: false,
524            ip6_dotint: false,
525            edns0: false,
526            single_request: false,
527            single_request_reopen: false,
528            no_tld_query: false,
529            use_vc: false,
530            no_reload: false,
531            trust_ad: false,
532            lookup: Vec::new(),
533            family: Vec::new(),
534            no_aaaa: false,
535        }
536    }
537}
538
539impl fmt::Display for Config {
540    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
541        let Self {
542            nameservers,
543            last_search,
544            domain,
545            search,
546            sortlist,
547            debug,
548            ndots,
549            timeout,
550            attempts,
551            rotate,
552            no_check_names,
553            inet6,
554            ip6_bytestring,
555            ip6_dotint,
556            edns0,
557            single_request,
558            single_request_reopen,
559            no_tld_query,
560            use_vc,
561            no_reload,
562            trust_ad,
563            lookup,
564            family,
565            no_aaaa,
566        } = self;
567
568        for nameserver in nameservers.iter() {
569            writeln!(fmt, "nameserver {nameserver}")?;
570        }
571
572        if last_search != &LastSearch::Domain {
573            if let Some(domain) = domain {
574                writeln!(fmt, "domain {domain}")?;
575            }
576        }
577
578        if let Some(search) = search {
579            if !search.is_empty() {
580                write!(fmt, "search")?;
581                for suffix in search.iter() {
582                    write!(fmt, " {suffix}")?;
583                }
584                writeln!(fmt)?;
585            }
586        }
587
588        if last_search == &LastSearch::Domain {
589            if let Some(domain) = &self.domain {
590                writeln!(fmt, "domain {domain}")?;
591            }
592        }
593
594        if !sortlist.is_empty() {
595            write!(fmt, "sortlist")?;
596            for network in sortlist.iter() {
597                write!(fmt, " {network}")?;
598            }
599            writeln!(fmt)?;
600        }
601
602        if !lookup.is_empty() {
603            write!(fmt, "lookup")?;
604            for db in lookup.iter() {
605                match db {
606                    Lookup::File => write!(fmt, " file")?,
607                    Lookup::Bind => write!(fmt, " bind")?,
608                    Lookup::Extra(extra) => write!(fmt, " {extra}")?,
609                }
610            }
611            writeln!(fmt)?;
612        }
613
614        if !family.is_empty() {
615            write!(fmt, "family")?;
616            for fam in family.iter() {
617                match fam {
618                    Family::Inet4 => write!(fmt, " inet4")?,
619                    Family::Inet6 => write!(fmt, " inet6")?,
620                }
621            }
622            writeln!(fmt)?;
623        }
624
625        if *debug {
626            writeln!(fmt, "options debug")?;
627        }
628        if *ndots != 1 {
629            writeln!(fmt, "options ndots:{}", self.ndots)?;
630        }
631        if *timeout != 5 {
632            writeln!(fmt, "options timeout:{}", self.timeout)?;
633        }
634        if *attempts != 2 {
635            writeln!(fmt, "options attempts:{}", self.attempts)?;
636        }
637        if *rotate {
638            writeln!(fmt, "options rotate")?;
639        }
640        if *no_check_names {
641            writeln!(fmt, "options no-check-names")?;
642        }
643        if *inet6 {
644            writeln!(fmt, "options inet6")?;
645        }
646        if *ip6_bytestring {
647            writeln!(fmt, "options ip6-bytestring")?;
648        }
649        if *ip6_dotint {
650            writeln!(fmt, "options ip6-dotint")?;
651        }
652        if *edns0 {
653            writeln!(fmt, "options edns0")?;
654        }
655        if *single_request {
656            writeln!(fmt, "options single-request")?;
657        }
658        if *single_request_reopen {
659            writeln!(fmt, "options single-request-reopen")?;
660        }
661        if *no_tld_query {
662            writeln!(fmt, "options no-tld-query")?;
663        }
664        if *use_vc {
665            writeln!(fmt, "options use-vc")?;
666        }
667        if *no_reload {
668            writeln!(fmt, "options no-reload")?;
669        }
670        if *trust_ad {
671            writeln!(fmt, "options trust-ad")?;
672        }
673        if *no_aaaa {
674            writeln!(fmt, "options no-aaaa")?;
675        }
676
677        Ok(())
678    }
679}
680
681/// Error while parsing resolv.conf file
682#[derive(Debug)]
683pub enum ParseError {
684    /// Error that may be returned when the string to parse contains invalid UTF-8 sequences
685    InvalidUtf8(usize, Utf8Error),
686    /// Error returned a value for a given directive is invalid.
687    /// This can also happen when the value is missing, if the directive requires a value.
688    InvalidValue(usize),
689    /// Error returned when a value for a given option is invalid.
690    /// This can also happen when the value is missing, if the option requires a value.
691    InvalidOptionValue(usize),
692    /// Error returned when a invalid option is found.
693    InvalidOption(usize),
694    /// Error returned when a invalid directive is found.
695    InvalidDirective(usize),
696    /// Error returned when a value cannot be parsed an an IP address.
697    InvalidIp(usize, AddrParseError),
698    /// Error returned when there is extra data at the end of a line.
699    ExtraData(usize),
700}
701
702impl std::fmt::Display for ParseError {
703    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
704        match self {
705            Self::InvalidUtf8(line, err) => write!(f, "bad unicode at line {line}: {err}"),
706            Self::InvalidValue(line) => write!(
707                f,
708                "directive at line {line} is improperly formatted or contains invalid value",
709            ),
710            Self::InvalidOptionValue(line) => write!(
711                f,
712                "directive options at line {line} contains invalid value of some option",
713            ),
714            Self::InvalidOption(line) => {
715                write!(f, "option at line {line} is not recognized")
716            }
717            Self::InvalidDirective(line) => {
718                write!(f, "directive at line {line} is not recognized")
719            }
720            Self::InvalidIp(line, err) => {
721                write!(f, "directive at line {line} contains invalid IP: {err}")
722            }
723            Self::ExtraData(line) => write!(f, "extra data at the end of line {line}"),
724        }
725    }
726}
727
728impl std::error::Error for ParseError {
729    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
730        match self {
731            Self::InvalidUtf8(_, err) => Some(err),
732            _ => None,
733        }
734    }
735}
736
737/// An iterator returned by [`Config.get_last_search_or_domain`](struct.Config.html#method.get_last_search_or_domain)
738#[derive(Debug, Clone)]
739pub struct DomainIter<'a>(DomainIterInternal<'a>);
740
741impl<'a> Iterator for DomainIter<'a> {
742    type Item = &'a String;
743
744    fn next(&mut self) -> Option<Self::Item> {
745        self.0.next()
746    }
747}
748
749#[derive(Debug, Clone)]
750enum DomainIterInternal<'a> {
751    Search(Option<Iter<'a, String>>),
752    Domain(Option<&'a String>),
753    None,
754}
755
756impl<'a> Iterator for DomainIterInternal<'a> {
757    type Item = &'a String;
758
759    fn next(&mut self) -> Option<Self::Item> {
760        match self {
761            DomainIterInternal::Search(Some(domains)) => domains.next(),
762            DomainIterInternal::Domain(domain) => domain.take(),
763            _ => None,
764        }
765    }
766}
767
768/// The databases that should be searched during a lookup.
769/// This option is commonly found on openbsd.
770#[derive(Clone, Debug, PartialEq, Eq)]
771pub enum Lookup {
772    /// Search for entries in /etc/hosts
773    File,
774    /// Query a domain name server
775    Bind,
776    /// A database we don't know yet
777    Extra(String),
778}
779
780/// The internet protocol family that is prefered.
781/// This option is commonly found on openbsd.
782#[derive(Clone, Debug, PartialEq, Eq)]
783pub enum Family {
784    /// A A lookup for an ipv4 address
785    Inet4,
786    /// A AAAA lookup for an ipv6 address
787    Inet6,
788}
789
790/// Parses the domain name from a hostname, if available
791fn domain_from_host(hostname: &[u8]) -> Option<&str> {
792    let mut start = None;
793    for (i, b) in hostname.iter().copied().enumerate() {
794        if b == b'.' && start.is_none() {
795            start = Some(i);
796            continue;
797        } else if b > 0 {
798            continue;
799        }
800
801        return match start? {
802            // Avoid empty domains
803            start if i - start < 2 => None,
804            start => str::from_utf8(&hostname[start + 1..i]).ok(),
805        };
806    }
807
808    None
809}
810
811#[derive(Copy, Clone, PartialEq, Eq, Debug)]
812enum LastSearch {
813    None,
814    Domain,
815    Search,
816}
817
818const NAMESERVER_LIMIT: usize = 3;
819const SEARCH_LIMIT: usize = 6;
820
821#[cfg(test)]
822mod test {
823    use super::domain_from_host;
824    #[test]
825    fn parses_domain_name() {
826        assert!(domain_from_host(b"regular-hostname\0").is_none());
827
828        assert_eq!(domain_from_host(b"with.domain-name\0"), Some("domain-name"));
829        assert_eq!(
830            domain_from_host(b"with.multiple.dots\0"),
831            Some("multiple.dots")
832        );
833
834        assert!(domain_from_host(b"hostname.\0").is_none());
835        assert_eq!(domain_from_host(b"host.a\0"), Some("a"));
836        assert_eq!(domain_from_host(b"host.au\0"), Some("au"));
837    }
838}