//! Query network interface addresses
//!
//! Uses the Linux and/or BSD specific function `getifaddrs` to query the list
//! of interfaces and their associated addresses.

use cfg_if::cfg_if;
#[cfg(any(target_os = "ios", target_os = "macos"))]
use std::convert::TryFrom;
use std::ffi;
use std::iter::Iterator;
use std::mem;
use std::option::Option;

use crate::net::if_::*;
use crate::sys::socket::{SockaddrLike, SockaddrStorage};
use crate::{Errno, Result};

/// Describes a single address for an interface as returned by `getifaddrs`.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct InterfaceAddress {
    /// Name of the network interface
    pub interface_name: String,
    /// Flags as from `SIOCGIFFLAGS` ioctl
    pub flags: InterfaceFlags,
    /// Network address of this interface
    pub address: Option<SockaddrStorage>,
    /// Netmask of this interface
    pub netmask: Option<SockaddrStorage>,
    /// Broadcast address of this interface, if applicable
    pub broadcast: Option<SockaddrStorage>,
    /// Point-to-point destination address
    pub destination: Option<SockaddrStorage>,
}

cfg_if! {
    if #[cfg(any(target_os = "android", target_os = "emscripten", target_os = "fuchsia", target_os = "linux"))] {
        fn get_ifu_from_sockaddr(info: &libc::ifaddrs) -> *const libc::sockaddr {
            info.ifa_ifu
        }
    } else {
        fn get_ifu_from_sockaddr(info: &libc::ifaddrs) -> *const libc::sockaddr {
            info.ifa_dstaddr
        }
    }
}

/// Workaround a bug in XNU where netmasks will always have the wrong size in
/// the sa_len field due to the kernel ignoring trailing zeroes in the structure
/// when setting the field. See https://github.com/nix-rust/nix/issues/1709#issuecomment-1199304470
///
/// To fix this, we stack-allocate a new sockaddr_storage, zero it out, and
/// memcpy sa_len of the netmask to that new storage. Finally, we reset the
/// ss_len field to sizeof(sockaddr_storage). This is supposedly valid as all
/// members of the sockaddr_storage are "ok" with being zeroed out (there are
/// no pointers).
#[cfg(any(target_os = "ios", target_os = "macos"))]
unsafe fn workaround_xnu_bug(info: &libc::ifaddrs) -> Option<SockaddrStorage> {
    let src_sock = info.ifa_netmask;
    if src_sock.is_null() {
        return None;
    }

    let mut dst_sock = mem::MaybeUninit::<libc::sockaddr_storage>::zeroed();

    // memcpy only sa_len bytes, assume the rest is zero
    std::ptr::copy_nonoverlapping(
        src_sock as *const u8,
        dst_sock.as_mut_ptr() as *mut u8,
        (*src_sock).sa_len.into(),
    );

    // Initialize ss_len to sizeof(libc::sockaddr_storage).
    (*dst_sock.as_mut_ptr()).ss_len =
        u8::try_from(mem::size_of::<libc::sockaddr_storage>()).unwrap();
    let dst_sock = dst_sock.assume_init();

    let dst_sock_ptr =
        &dst_sock as *const libc::sockaddr_storage as *const libc::sockaddr;

    SockaddrStorage::from_raw(dst_sock_ptr, None)
}

impl InterfaceAddress {
    /// Create an `InterfaceAddress` from the libc struct.
    fn from_libc_ifaddrs(info: &libc::ifaddrs) -> InterfaceAddress {
        let ifname = unsafe { ffi::CStr::from_ptr(info.ifa_name) };
        let address = unsafe { SockaddrStorage::from_raw(info.ifa_addr, None) };
        #[cfg(any(target_os = "ios", target_os = "macos"))]
        let netmask = unsafe { workaround_xnu_bug(info) };
        #[cfg(not(any(target_os = "ios", target_os = "macos")))]
        let netmask =
            unsafe { SockaddrStorage::from_raw(info.ifa_netmask, None) };
        let mut addr = InterfaceAddress {
            interface_name: ifname.to_string_lossy().to_string(),
            flags: InterfaceFlags::from_bits_truncate(info.ifa_flags as i32),
            address,
            netmask,
            broadcast: None,
            destination: None,
        };

        let ifu = get_ifu_from_sockaddr(info);
        if addr.flags.contains(InterfaceFlags::IFF_POINTOPOINT) {
            addr.destination = unsafe { SockaddrStorage::from_raw(ifu, None) };
        } else if addr.flags.contains(InterfaceFlags::IFF_BROADCAST) {
            addr.broadcast = unsafe { SockaddrStorage::from_raw(ifu, None) };
        }

        addr
    }
}

/// Holds the results of `getifaddrs`.
///
/// Use the function `getifaddrs` to create this Iterator. Note that the
/// actual list of interfaces can be iterated once and will be freed as
/// soon as the Iterator goes out of scope.
#[derive(Debug, Eq, Hash, PartialEq)]
pub struct InterfaceAddressIterator {
    base: *mut libc::ifaddrs,
    next: *mut libc::ifaddrs,
}

impl Drop for InterfaceAddressIterator {
    fn drop(&mut self) {
        unsafe { libc::freeifaddrs(self.base) };
    }
}

impl Iterator for InterfaceAddressIterator {
    type Item = InterfaceAddress;
    fn next(&mut self) -> Option<<Self as Iterator>::Item> {
        match unsafe { self.next.as_ref() } {
            Some(ifaddr) => {
                self.next = ifaddr.ifa_next;
                Some(InterfaceAddress::from_libc_ifaddrs(ifaddr))
            }
            None => None,
        }
    }
}

/// Get interface addresses using libc's `getifaddrs`
///
/// Note that the underlying implementation differs between OSes. Only the
/// most common address families are supported by the nix crate (due to
/// lack of time and complexity of testing). The address family is encoded
/// in the specific variant of `SockaddrStorage` returned for the fields
/// `address`, `netmask`, `broadcast`, and `destination`. For any entry not
/// supported, the returned list will contain a `None` entry.
///
/// # Example
/// ```
/// let addrs = nix::ifaddrs::getifaddrs().unwrap();
/// for ifaddr in addrs {
///   match ifaddr.address {
///     Some(address) => {
///       println!("interface {} address {}",
///                ifaddr.interface_name, address);
///     },
///     None => {
///       println!("interface {} with unsupported address family",
///                ifaddr.interface_name);
///     }
///   }
/// }
/// ```
pub fn getifaddrs() -> Result<InterfaceAddressIterator> {
    let mut addrs = mem::MaybeUninit::<*mut libc::ifaddrs>::uninit();
    unsafe {
        Errno::result(libc::getifaddrs(addrs.as_mut_ptr())).map(|_| {
            InterfaceAddressIterator {
                base: addrs.assume_init(),
                next: addrs.assume_init(),
            }
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Only checks if `getifaddrs` can be invoked without panicking.
    #[test]
    fn test_getifaddrs() {
        let _ = getifaddrs();
    }

    // Ensures getting the netmask works, and in particular that
    // `workaround_xnu_bug` works properly.
    #[test]
    fn test_getifaddrs_netmask_correct() {
        let addrs = getifaddrs().unwrap();
        for iface in addrs {
            let sock = if let Some(sock) = iface.netmask {
                sock
            } else {
                continue;
            };
            if sock.family() == Some(crate::sys::socket::AddressFamily::Inet) {
                let _ = sock.as_sockaddr_in().unwrap();
                return;
            } else if sock.family()
                == Some(crate::sys::socket::AddressFamily::Inet6)
            {
                let _ = sock.as_sockaddr_in6().unwrap();
                return;
            }
        }
        panic!("No address?");
    }
}
