//! Parse an IP address or a CIDR specification.
/*
 * Copyright (c) 2021  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#![allow(clippy::module_name_repetitions)]

use anyhow::anyhow;
use nom::{
    bytes::complete::tag,
    character::complete::{none_of, u32 as p_u32, u8 as p_u8},
    combinator::{all_consuming, opt},
    error::{Error as NError, ErrorKind as NErrorKind},
    multi::separated_list0,
    sequence::{preceded, separated_pair, tuple},
    Err as NErr, IResult,
};

use crate::defs::{AddrExclude, AddrRange, Error};

/// Make a `nom` error suitable for using as an `anyhow` error.
fn clone_err_input(err: NErr<NError<&str>>) -> NErr<NError<String>> {
    err.map_input(ToOwned::to_owned)
}

/// Utility function for building up a Nom failure error.
fn _err_fail(input: &str) -> NErr<NError<&str>> {
    NErr::Failure(NError::new(input, NErrorKind::Fail))
}

fn _p_excl_single(input: &str) -> IResult<&str, Vec<u8>> {
    separated_list0(none_of("0123456789."), p_u8)(input)
}

/// Parse an exclusion pattern.
fn _p_exclude(input: &str) -> IResult<&str, Vec<AddrExclude>> {
    let (r_input, (first, second_opt, third_opt, fourth_opt)) = tuple((
        _p_excl_single,
        opt(preceded(tag("."), _p_excl_single)),
        opt(preceded(tag("."), _p_excl_single)),
        opt(preceded(tag("."), _p_excl_single)),
    ))(input)?;

    #[allow(clippy::little_endian_bytes)]
    let excl = {
        let excl_first = first
            .into_iter()
            .map(|excl| AddrExclude::new(0xFF_00_00_00_u32, u32::from_le_bytes([0, 0, 0, excl])));
        let excl_second = second_opt
            .map_or_else(|| vec![].into_iter(), Vec::into_iter)
            .map(|excl| AddrExclude::new(0x00_FF_00_00, u32::from_le_bytes([0, 0, excl, 0])));
        let excl_third = third_opt
            .map_or_else(|| vec![].into_iter(), Vec::into_iter)
            .map(|excl| AddrExclude::new(0x00_00_FF_00, u32::from_le_bytes([0, excl, 0, 0])));
        let excl_fourth = fourth_opt
            .map_or_else(|| vec![].into_iter(), Vec::into_iter)
            .map(|excl| AddrExclude::new(0x00_00_00_FF, u32::from_le_bytes([excl, 0, 0, 0])));

        excl_first
            .chain(excl_second)
            .chain(excl_third)
            .chain(excl_fourth)
            .collect::<Vec<_>>()
    };
    Ok((r_input, excl))
}

/// Parse an exclusion pattern into a series of [`AddrExclude`] objects.
pub fn parse_exclude(pattern: &str) -> Result<Vec<AddrExclude>, Error> {
    match all_consuming(_p_exclude)(pattern).map_err(clone_err_input) {
        Ok((_, excl)) => Ok(excl),
        Err(err) => Err(Error::ParseExclude(pattern.to_owned(), err.into())),
    }
}

/// Parse an IPv4 address into a [`u32`].
fn _p_ipv4(input: &str) -> IResult<&str, u32> {
    let (r_input, (first, second, third, fourth)) = tuple((
        p_u8,
        preceded(tag("."), p_u8),
        preceded(tag("."), p_u8),
        preceded(tag("."), p_u8),
    ))(input)?;
    #[allow(clippy::little_endian_bytes)]
    Ok((r_input, u32::from_le_bytes([fourth, third, second, first])))
}

/// Parse an addr/prefixlen CIDR specification.
fn _p_cidr(input: &str) -> IResult<&str, (u32, u32)> {
    separated_pair(_p_ipv4, tag("/"), p_u32)(input)
}

/// Parse an IPv4 address into an unsigned 32-bit number.
pub fn parse_addr(saddr: &str) -> Result<u32, Error> {
    match all_consuming(_p_ipv4)(saddr) {
        Ok((_, res)) => Ok(res),
        Err(err) => Err(Error::ParseAddress(
            saddr.to_owned(),
            clone_err_input(err).into(),
        )),
    }
}

/// Parse an address/prefixlen specification into an address range.
pub fn parse_cidr(spec: &str) -> Result<AddrRange, Error> {
    let (start, prefix_len) = match all_consuming(_p_cidr)(spec) {
        Ok((_, res)) => res,
        Err(err) => {
            return Err(Error::ParseCidr(
                spec.to_owned(),
                clone_err_input(err).into(),
            ))
        }
    };

    let err_internal = || {
        Error::Internal(anyhow!(
            "Unexpected arithmetic operation error for {start}/{prefix_len}"
        ))
    };
    match prefix_len {
        0 => {
            if start == 0 {
                Ok(AddrRange {
                    start,
                    end: u32::MAX,
                })
            } else {
                Err(Error::CidrBadStart)
            }
        }
        value if value < 32 => {
            let offset = 1_u32
                .checked_shl(32_u32.checked_sub(value).ok_or_else(err_internal)?)
                .ok_or_else(err_internal)?;
            let mask = offset.checked_sub(1).ok_or_else(err_internal)?;
            if (start & mask) == 0 {
                Ok(AddrRange {
                    start,
                    end: start.checked_add(mask).ok_or_else(err_internal)?,
                })
            } else {
                Err(Error::CidrBadStart)
            }
        }
        32 => Ok(AddrRange { start, end: start }),
        _ => Err(Error::CidrPrefixTooLarge),
    }
}

/// Parse two IPv4 addresses into a range.
pub fn parse_range(first: &str, second: &str) -> Result<AddrRange, Error> {
    let start = parse_addr(first)?;
    let end = parse_addr(second)?;
    if start <= end {
        Ok(AddrRange { start, end })
    } else {
        Err(Error::StartBeforeEnd)
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use rstest::rstest;

    use crate::defs::AddrExclude;

    #[rstest]
    #[case("...", vec![])]
    #[case("...1", vec![AddrExclude::new(0x00_00_00_FF, 0x00_00_00_01 )])]
    #[case("..1.2,3", vec![
        AddrExclude::new(0x00_00_FF_00, 0x00_00_01_00),
        AddrExclude::new(0x00_00_00_FF, 0x00_00_00_02),
        AddrExclude::new(0x00_00_00_FF, 0x00_00_00_03),
    ])]
    fn test_parse(#[case] value: &str, #[case] expected: Vec<AddrExclude>) {
        assert_eq!(super::parse_exclude(value).unwrap(), expected);
    }
}
