cdns-rs 1.2.2

A native Sync/Async Rust implementation of client DNS resolver.
Documentation
/*-
 * cdns-rs - a simple sync/async DNS query library
 * 
 * Copyright (C) 2020  Aleksandr Morozov
 * 
 * Copyright (C) 2025 Aleksandr Morozov
 * 
 * The syslog-rs crate can be redistributed and/or modified
 * under the terms of either of the following licenses:
 *
 *   1. the Mozilla Public License Version 2.0 (the “MPL”) OR
 *                     
 *   2. EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016
 */

/// This file contains the config file parsers.

use std::borrow::Borrow;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::net::IpAddr;

use crate::common::HOST_CFG_PATH;
use crate::{error::*, internal_error_map, write_error, QType};
use crate::tokenizer::*;


/// An /etc/hosts file parser


#[derive(Clone, Debug)]
pub struct HostnameEntry
{
    ip: IpAddr,
    hostnames: Vec<String>,
}

impl Eq for HostnameEntry {}

impl PartialEq for HostnameEntry 
{
    fn eq(&self, other: &HostnameEntry) -> bool 
    {
        return self.ip == other.ip;
    }
}

impl Borrow<IpAddr> for HostnameEntry
{
    fn borrow(&self) -> &IpAddr 
    {
        return &self.ip;    
    }
}

impl Hash for HostnameEntry 
{
    fn hash<H: Hasher>(&self, state: &mut H) 
    {
        self.ip.hash(state);
    }
}


impl HostnameEntry
{
    /// Returns the IP address
    pub 
    fn get_ip(&self) -> &IpAddr
    {
        return &self.ip;
    }

    /// Returns the slice with the hostnames
    pub 
    fn get_hostnames(&self) -> &[String]
    {
        return self.hostnames.as_slice();
    }

    /// Returns the iterator of the hostnames
    pub 
    fn get_hostnames_iter(&self) -> std::slice::Iter<'_, String>
    {
        return self.hostnames.iter();
    }
}

#[derive(Clone, Debug)]
pub struct HostConfig
{
    hostnames: HashSet<HostnameEntry>
}

impl Default for HostConfig
{
    fn default() -> Self 
    {
        return
            Self 
            { 
                hostnames: Default::default(), 
            };
    }
}

impl HostConfig
{
    pub 
    fn is_empty(&self) -> bool
    {
        return self.hostnames.is_empty();
    }

    pub 
    fn search_by_ip(&self, ip: &IpAddr) -> Option<&HostnameEntry>
    {
        return self.hostnames.get(ip);
    }

    // Expensive operation. Walks through the list in On(Ologn) or On^2
    pub 
    fn search_by_fqdn(&self, qtype: &QType, name: &str) -> Option<&HostnameEntry>
    {
        for host in self.hostnames.iter()
        {
            for fqdn in host.hostnames.iter()
            {
                // check that fqdn match and IP type is same as requested
                if name == fqdn.as_str() && qtype.ipaddr_match(&host.ip)
                {
                    return Some(host);
                }
            }
        }

        return None;
    }

    pub 
    fn parse_host_file_internal(file_content: String) -> CDnsResult<Self>
    {
        let mut tk = ConfTokenizer::from_str(&file_content)?;
        let mut he_list: HashSet<HostnameEntry> = HashSet::new();

        loop
        {
            let field_ip = tk.read_next()?;
    
            if field_ip.is_none() == true
            {
                // reached EOF
                break;
            }

            //println!("ip: {}", field_ip.as_ref().unwrap());
    
            let ip: IpAddr = 
                match field_ip.unwrap().parse()
                {
                    Ok(r) => r,
                    Err(_e) => 
                    {
                        // skip
                        tk.skip_upto_eol();
                        continue;
                    }
                };
    
            let hostnames = tk.read_upto_eol()?;

            if hostnames.len() > 0
            {
                let he = HostnameEntry{ ip: ip, hostnames: hostnames };
                he_list.insert(he);
            }
            else
            {
                write_error!(
                    internal_error_map!(CDnsErrorType::ConfigError, 
                        "in file: '{}' IP is not defined with domain name: '{}'\n", HOST_CFG_PATH, ip)
                );
            }
        }

        if he_list.len() == 0
        {
            write_error!(
                internal_error_map!(CDnsErrorType::ConfigError, "file: '{}' file is empty or damaged!\n", 
                    HOST_CFG_PATH)
            );
        }

        return Ok(
            Self
            {
                hostnames: he_list
            }
        );
    }    
}

#[cfg(test)]
mod tests
{
    use std::net::IpAddr;

    use crate::cfg_host_parser::HostConfig;
    
    #[test]
    fn test_parse_host_file_0()
    {
        let hosts1: Vec<&'static str> = vec!["debian-laptop"];
        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];

        let ip1: IpAddr = "127.0.1.1".parse().unwrap();
        let ip2: IpAddr = "::1".parse().unwrap();
        let ip3: IpAddr = "ff02::1".parse().unwrap();
        let ip4: IpAddr = "ff02::2".parse().unwrap();

        let ip_list = 
            vec![
                (ip1, hosts1), 
                (ip2, hosts2), 
                (ip3, hosts3),
                (ip4, hosts4)
            ];

        let test =
        "127.0. 0.1	localhost
        127.0.1.1	debian-laptop
        
        # The following lines are desirable for IPv6 capable hosts
        ::1     localhost ip6-localhost ip6-loopback
        ff02::1 ip6-allnodes
        ff02::2 ip6-allrouters".to_string();

        let p = HostConfig::parse_host_file_internal(test);
        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());

        let p = p.unwrap();

        for (ip, host) in ip_list
        {
            let res = p.hostnames.get(&ip);
            assert_eq!(res.is_some(), true);

            let res = res.unwrap();

            assert_eq!(res.hostnames, host);
        }

        return;
    }

    #[test]
    fn test_parse_host_file()
    {

        let ip0:IpAddr = "127.0.0.1".parse().unwrap();
        let ip1:IpAddr = "127.0.1.1".parse().unwrap();
        let ip2:IpAddr = "::1".parse().unwrap();
        let ip3:IpAddr = "ff02::1".parse().unwrap();
        let ip4:IpAddr = "ff02::2".parse().unwrap();

        let hosts0: Vec<&'static str> = vec!["localhost"];
        let hosts1: Vec<&'static str> = vec!["debian-laptop"];
        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];

        let ip_list = 
            vec![
                (ip0, hosts0),
                (ip1, hosts1), 
                (ip2, hosts2), 
                (ip3, hosts3),
                (ip4, hosts4)
            ];

        let test =
        "127.0.0.1	localhost
        127.0.1.1	debian-laptop
        
        # The following lines are desirable for IPv6 capable hosts
        ::1     localhost ip6-localhost ip6-loopback
        ff02::1 ip6-allnodes
        ff02::2 ip6-allrouters".to_string();


        let p = HostConfig::parse_host_file_internal(test);
        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());

        let p = p.unwrap();

        for (ip, host) in ip_list
        {
            let res = p.hostnames.get(&ip);
            assert_eq!(res.is_some(), true);

            let res = res.unwrap();

            assert_eq!(res.hostnames, host);
        }
    }

    #[test]
    fn test_parse_host_file_2()
    {

        let ip0:IpAddr = "127.0.0.1".parse().unwrap();
        let ip1:IpAddr = "127.0.1.1".parse().unwrap();
        let ip2:IpAddr = "::1".parse().unwrap();
        let ip3:IpAddr = "ff02::1".parse().unwrap();
        let ip4:IpAddr = "ff02::2".parse().unwrap();

        let hosts0: Vec<&'static str> = vec!["localhost", "localdomain", "domain.local"];
        let hosts1: Vec<&'static str> = vec!["debian-laptop", "test123.domain.tld"];
        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];


        let ip_list = 
            vec![
                (ip0, hosts0),
                (ip1, hosts1), 
                (ip2, hosts2), 
                (ip3, hosts3),
                (ip4, hosts4)
            ];

        let test =
        "127.0.0.1	localhost localdomain domain.local
        127.0.1.1	debian-laptop test123.domain.tld
        
        # The following lines are desirable for IPv6 capable hosts
        #
        #
        ::1     localhost ip6-localhost ip6-loopback
        ff02::1 ip6-allnodes
        ff02::2 ip6-allrouters
        ".to_string();

        let p = HostConfig::parse_host_file_internal(test);
        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());

        let p = p.unwrap();

        for (ip, host) in ip_list
        {
            let res = p.hostnames.get(&ip);
            assert_eq!(res.is_some(), true);

            let res = res.unwrap();

            assert_eq!(res.hostnames, host);
        }
    }

}