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}