/* Copyright (C) 2023 Open Information Security Foundation
 *
 * You can copy, redistribute or modify this Program under the terms of
 * the GNU General Public License version 2 as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * version 2 along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

use std::collections::{HashSet, VecDeque};
use std::{cmp::Ordering, ffi::CStr};

// std::ffi::{c_char, c_int} is recommended these days, but requires
// Rust 1.64.0.
use std::os::raw::{c_char, c_int};

use nom7::bytes::complete::take_while;
use nom7::combinator::map;
use nom7::multi::{many1, separated_list1};
use nom7::sequence::tuple;
use nom7::{
    branch::alt,
    bytes::complete::{tag, take_till},
    character::complete::{char, multispace0},
    combinator::map_res,
    sequence::preceded,
    IResult,
};

#[derive(Debug, Eq, PartialEq)]
enum RequiresError {
    /// Suricata is greater than the required version.
    VersionGt,

    /// Suricata is less than the required version.
    VersionLt(SuricataVersion),

    /// The running Suricata is missing a required feature.
    MissingFeature(String),

    /// The Suricata version, of Suricata itself is bad and failed to parse.
    BadSuricataVersion,

    /// The requires expression is bad and failed to parse.
    BadRequires,

    /// MultipleVersions
    MultipleVersions,

    /// Passed in requirements not a valid UTF-8 string.
    Utf8Error,
}

impl RequiresError {
    /// Return a pointer to a C compatible constant error message.
    const fn c_errmsg(&self) -> *const c_char {
        let msg = match self {
            Self::VersionGt => "Suricata version greater than required\0",
            Self::VersionLt(_) => "Suricata version less than required\0",
            Self::MissingFeature(_) => "Suricata missing a required feature\0",
            Self::BadSuricataVersion => "Failed to parse running Suricata version\0",
            Self::BadRequires => "Failed to parse requires expression\0",
            Self::MultipleVersions => "Version may only be specified once\0",
            Self::Utf8Error => "Requires expression is not valid UTF-8\0",
        };
        msg.as_ptr() as *const c_char
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
enum VersionCompareOp {
    Gt,
    Gte,
    Lt,
    Lte,
}

#[derive(Debug, Clone, Eq, PartialEq)]
struct SuricataVersion {
    major: u8,
    minor: u8,
    patch: u8,
}

impl PartialOrd for SuricataVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for SuricataVersion {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.major.cmp(&other.major) {
            Ordering::Equal => match self.minor.cmp(&other.minor) {
                Ordering::Equal => self.patch.cmp(&other.patch),
                other => other,
            },
            other => other,
        }
    }
}

impl std::fmt::Display for SuricataVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

impl SuricataVersion {
    fn new(major: u8, minor: u8, patch: u8) -> Self {
        Self {
            major,
            minor,
            patch,
        }
    }
}

/// Parse a version expression.
///
/// Parse into a version expression into a nested array, for example:
///
///    version: >= 7.0.3 < 8 | >= 8.0.3
///
/// would result in something like:
///
/// [
///     [{op: gte, version: 7.0.3}, {op:lt, version: 8}],
///     [{op: gte, version: 8.0.3}],
/// ]
fn parse_version_expression(input: &str) -> IResult<&str, Vec<Vec<RuleRequireVersion>>> {
    let sep = preceded(multispace0, tag("|"));
    let inner_parser = many1(tuple((parse_op, parse_version)));
    let (input, versions) = separated_list1(sep, inner_parser)(input)?;

    let versions = versions
        .into_iter()
        .map(|versions| {
            versions
                .into_iter()
                .map(|(op, version)| RuleRequireVersion { op, version })
                .collect()
        })
        .collect();

    Ok((input, versions))
}

#[derive(Debug, Eq, PartialEq)]
struct RuleRequireVersion {
    pub op: VersionCompareOp,
    pub version: SuricataVersion,
}

#[derive(Debug, Default, Eq, PartialEq)]
struct Requires {
    pub features: Vec<String>,

    /// The version expression.
    ///
    /// - All of the inner most must evaluate to true.
    /// - To pass, any of the outer must be true.
    pub version: Vec<Vec<RuleRequireVersion>>,
}

fn parse_op(input: &str) -> IResult<&str, VersionCompareOp> {
    preceded(
        multispace0,
        alt((
            map(tag(">="), |_| VersionCompareOp::Gte),
            map(tag(">"), |_| VersionCompareOp::Gt),
            map(tag("<="), |_| VersionCompareOp::Lte),
            map(tag("<"), |_| VersionCompareOp::Lt),
        )),
    )(input)
}

/// Parse the next part of the version.
///
/// That is all chars up to eof, or the next '.' or '-'.
fn parse_next_version_part(input: &str) -> IResult<&str, u8> {
    map_res(
        take_till(|c| c == '.' || c == '-' || c == ' '),
        |s: &str| s.parse::<u8>(),
    )(input)
}

/// Parse a version string into a SuricataVersion.
fn parse_version(input: &str) -> IResult<&str, SuricataVersion> {
    let (input, major) = preceded(multispace0, parse_next_version_part)(input)?;
    let (input, minor) = if input.is_empty() || input.starts_with(' ') {
        (input, 0)
    } else {
        preceded(char('.'), parse_next_version_part)(input)?
    };
    let (input, patch) = if input.is_empty() || input.starts_with(' ') {
        (input, 0)
    } else {
        preceded(char('.'), parse_next_version_part)(input)?
    };

    Ok((input, SuricataVersion::new(major, minor, patch)))
}

fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> {
    // Parse the keyword, any sequence of characters, numbers or "-" or "_".
    let (input, key) = preceded(
        multispace0,
        take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
    )(input)?;
    let (input, value) = preceded(multispace0, take_till(|c: char| c == ','))(input)?;
    Ok((input, (key, value)))
}

fn parse_requires(mut input: &str) -> Result<Requires, RequiresError> {
    let mut requires = Requires::default();

    while !input.is_empty() {
        let (rest, (keyword, value)) =
            parse_key_value(input).map_err(|_| RequiresError::BadRequires)?;
        match keyword {
            "feature" => {
                requires.features.push(value.trim().to_string());
            }
            "version" => {
                if !requires.version.is_empty() {
                    return Err(RequiresError::MultipleVersions);
                }
                let (_, versions) =
                    parse_version_expression(value).map_err(|_| RequiresError::BadRequires)?;
                requires.version = versions;
            }
            _ => {
                // Unknown keyword, allow by warn in case we extend
                // this in the future.
                SCLogWarning!("Unknown requires keyword: {}", keyword);
            }
        }

        // No consume any remaining ',' or whitespace.
        input = rest.trim_start_matches(|c: char| c == ',' || c.is_whitespace());
    }
    Ok(requires)
}

fn parse_suricata_version(version: &CStr) -> Result<SuricataVersion, *const c_char> {
    let version = version
        .to_str()
        .map_err(|_| RequiresError::BadSuricataVersion.c_errmsg())?;
    let (_, version) =
        parse_version(version).map_err(|_| RequiresError::BadSuricataVersion.c_errmsg())?;
    Ok(version)
}

fn check_version(
    version: &RuleRequireVersion, suricata_version: &SuricataVersion,
) -> Result<(), RequiresError> {
    match version.op {
        VersionCompareOp::Gt => {
            if suricata_version <= &version.version {
                return Err(RequiresError::VersionLt(version.version.clone()));
            }
        }
        VersionCompareOp::Gte => {
            if suricata_version < &version.version {
                return Err(RequiresError::VersionLt(version.version.clone()));
            }
        }
        VersionCompareOp::Lt => {
            if suricata_version >= &version.version {
                return Err(RequiresError::VersionGt);
            }
        }
        VersionCompareOp::Lte => {
            if suricata_version > &version.version {
                return Err(RequiresError::VersionGt);
            }
        }
    }
    Ok(())
}

fn check_requires(
    requires: &Requires, suricata_version: &SuricataVersion,
) -> Result<(), RequiresError> {
    if !requires.version.is_empty() {
        let mut errs = VecDeque::new();
        let mut ok = 0;
        for or_versions in &requires.version {
            let mut err = None;
            for version in or_versions {
                if let Err(_err) = check_version(version, suricata_version) {
                    err = Some(_err);
                    break;
                }
            }
            if let Some(err) = err {
                errs.push_back(err);
            } else {
                ok += 1;
            }
        }
        if ok == 0 {
            return Err(errs.pop_front().unwrap());
        }
    }

    for feature in &requires.features {
        if !crate::feature::requires(feature) {
            return Err(RequiresError::MissingFeature(feature.to_string()));
        }
    }

    Ok(())
}

/// Status object to hold required features and the latest version of
/// Suricata required.
///
/// Full qualified name as it is exposed to C.
#[derive(Debug, Default)]
pub struct SCDetectRequiresStatus {
    min_version: Option<SuricataVersion>,
    features: HashSet<String>,

    /// Number of rules that didn't meet a feature.
    feature_count: u64,

    /// Number of rules where the Suricata version wasn't new enough.
    lt_count: u64,

    /// Number of rules where the Suricata version was too new.
    gt_count: u64,
}

#[no_mangle]
pub extern "C" fn SCDetectRequiresStatusNew() -> *mut SCDetectRequiresStatus {
    Box::into_raw(Box::default())
}

#[no_mangle]
pub unsafe extern "C" fn SCDetectRequiresStatusFree(status: *mut SCDetectRequiresStatus) {
    if !status.is_null() {
        std::mem::drop(Box::from_raw(status));
    }
}

#[no_mangle]
pub unsafe extern "C" fn SCDetectRequiresStatusLog(
    status: &mut SCDetectRequiresStatus, suricata_version: *const c_char, tenant_id: u32,
) {
    let suricata_version = CStr::from_ptr(suricata_version)
        .to_str()
        .unwrap_or("<unknown>");

    let mut parts = vec![];
    if status.lt_count > 0 {
        let min_version = status
            .min_version
            .as_ref()
            .map(|v| v.to_string())
            .unwrap_or_else(|| "<unknown>".to_string());
        let msg = format!(
            "{} {} skipped because the running Suricata version {} is less than {}",
            status.lt_count,
            if status.lt_count > 1 {
                "rules were"
            } else {
                "rule was"
            },
            suricata_version,
            &min_version
        );
        parts.push(msg);
    }
    if status.gt_count > 0 {
        let msg = format!(
            "{} {} for an older version Suricata",
            status.gt_count,
            if status.gt_count > 1 {
                "rules were skipped as they are"
            } else {
                "rule was skipped as it is"
            }
        );
        parts.push(msg);
    }
    if status.feature_count > 0 {
        let features = status
            .features
            .iter()
            .map(|f| f.to_string())
            .collect::<Vec<String>>()
            .join(", ");
        let msg = format!(
            "{}{} {} skipped because the running Suricata version does not have feature{}: [{}]",
            if tenant_id > 0 {
                format!("tenant id: {}  ", tenant_id)
            } else {
                String::new()
            },
            status.feature_count,
            if status.feature_count > 1 {
                "rules were"
            } else {
                "rule was"
            },
            if status.feature_count > 1 { "s" } else { "" },
            &features
        );
        parts.push(msg);
    }

    let msg = parts.join("; ");

    if status.lt_count > 0 {
        SCLogNotice!("{}", &msg);
    } else if status.gt_count > 0 || status.feature_count > 0 {
        SCLogInfo!("{}", &msg);
    }
}

/// Parse a "requires" rule option.
///
/// Return values:
///   *  0 - OK, rule should continue loading
///   * -1 - Error parsing the requires content
///   * -4 - Requirements not met, don't continue loading the rule, this
///          value is chosen so it can be passed back to the options parser
///          as its treated as a non-fatal silent error.
#[no_mangle]
pub unsafe extern "C" fn SCDetectCheckRequires(
    requires: *const c_char, suricata_version_string: *const c_char, errstr: *mut *const c_char,
    status: &mut SCDetectRequiresStatus,
) -> c_int {
    // First parse the running Suricata version.
    let suricata_version = match parse_suricata_version(CStr::from_ptr(suricata_version_string)) {
        Ok(version) => version,
        Err(err) => {
            *errstr = err;
            return -1;
        }
    };

    let requires = match CStr::from_ptr(requires)
        .to_str()
        .map_err(|_| RequiresError::Utf8Error)
        .and_then(parse_requires)
    {
        Ok(requires) => requires,
        Err(err) => {
            *errstr = err.c_errmsg();
            return -1;
        }
    };

    match check_requires(&requires, &suricata_version) {
        Ok(()) => 0,
        Err(err) => {
            match &err {
                RequiresError::VersionLt(version) => {
                    if let Some(min_version) = &status.min_version {
                        if version > min_version {
                            status.min_version = Some(version.clone());
                        }
                    } else {
                        status.min_version = Some(version.clone());
                    }
                    status.lt_count += 1;
                }
                RequiresError::MissingFeature(feature) => {
                    status.features.insert(feature.to_string());
                    status.feature_count += 1;
                }
                RequiresError::VersionGt => {
                    status.gt_count += 1;
                }
                _ => {}
            }
            *errstr = err.c_errmsg();
            return -4;
        }
    }
}

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

    #[test]
    fn test_suricata_version() {
        // 7.1.1 < 7.1.2
        assert!(SuricataVersion::new(7, 1, 1) < SuricataVersion::new(7, 1, 2));

        // 7.1.1 <= 7.1.2
        assert!(SuricataVersion::new(7, 1, 1) <= SuricataVersion::new(7, 1, 2));

        // 7.1.1 <= 7.1.1
        assert!(SuricataVersion::new(7, 1, 1) <= SuricataVersion::new(7, 1, 1));

        // NOT 7.1.1 < 7.1.1
        assert!(SuricataVersion::new(7, 1, 1) >= SuricataVersion::new(7, 1, 1));

        // 7.3.1 < 7.22.1
        assert!(SuricataVersion::new(7, 3, 1) < SuricataVersion::new(7, 22, 1));

        // 7.22.1 >= 7.3.4
        assert!(SuricataVersion::new(7, 22, 1) >= SuricataVersion::new(7, 3, 4));
    }

    #[test]
    fn test_parse_op() {
        assert_eq!(parse_op(">").unwrap().1, VersionCompareOp::Gt);
        assert_eq!(parse_op(">=").unwrap().1, VersionCompareOp::Gte);
        assert_eq!(parse_op("<").unwrap().1, VersionCompareOp::Lt);
        assert_eq!(parse_op("<=").unwrap().1, VersionCompareOp::Lte);

        assert!(parse_op("=").is_err());
    }

    #[test]
    fn test_parse_version() {
        assert_eq!(
            parse_version("7").unwrap().1,
            SuricataVersion {
                major: 7,
                minor: 0,
                patch: 0,
            }
        );

        assert_eq!(
            parse_version("7.1").unwrap().1,
            SuricataVersion {
                major: 7,
                minor: 1,
                patch: 0,
            }
        );

        assert_eq!(
            parse_version("7.1.2").unwrap().1,
            SuricataVersion {
                major: 7,
                minor: 1,
                patch: 2,
            }
        );

        // Suricata pre-releases will have a suffix starting with a
        // '-', so make sure we accept those versions as well.
        assert_eq!(
            parse_version("8.0.0-dev").unwrap().1,
            SuricataVersion {
                major: 8,
                minor: 0,
                patch: 0,
            }
        );

        assert!(parse_version("7.1.2a").is_err());
        assert!(parse_version("a").is_err());
        assert!(parse_version("777").is_err());
        assert!(parse_version("product-1").is_err());
    }

    #[test]
    fn test_parse_requires() {
        let requires = parse_requires("  feature geoip").unwrap();
        assert_eq!(&requires.features[0], "geoip");

        let requires = parse_requires("  feature geoip,    feature    lua  ").unwrap();
        assert_eq!(&requires.features[0], "geoip");
        assert_eq!(&requires.features[1], "lua");

        let requires = parse_requires("version >=7").unwrap();
        assert_eq!(
            requires,
            Requires {
                features: vec![],
                version: vec![vec![RuleRequireVersion {
                    op: VersionCompareOp::Gte,
                    version: SuricataVersion {
                        major: 7,
                        minor: 0,
                        patch: 0,
                    }
                }]],
            }
        );

        let requires = parse_requires("version >= 7.1").unwrap();
        assert_eq!(
            requires,
            Requires {
                features: vec![],
                version: vec![vec![RuleRequireVersion {
                    op: VersionCompareOp::Gte,
                    version: SuricataVersion {
                        major: 7,
                        minor: 1,
                        patch: 0,
                    }
                }]],
            }
        );

        let requires = parse_requires("feature output::file-store, version >= 7.1.2").unwrap();
        assert_eq!(
            requires,
            Requires {
                features: vec!["output::file-store".to_string()],
                version: vec![vec![RuleRequireVersion {
                    op: VersionCompareOp::Gte,
                    version: SuricataVersion {
                        major: 7,
                        minor: 1,
                        patch: 2,
                    }
                }]],
            }
        );

        let requires = parse_requires("feature geoip, version >= 7.1.2 < 8").unwrap();
        assert_eq!(
            requires,
            Requires {
                features: vec!["geoip".to_string()],
                version: vec![vec![
                    RuleRequireVersion {
                        op: VersionCompareOp::Gte,
                        version: SuricataVersion {
                            major: 7,
                            minor: 1,
                            patch: 2,
                        },
                    },
                    RuleRequireVersion {
                        op: VersionCompareOp::Lt,
                        version: SuricataVersion {
                            major: 8,
                            minor: 0,
                            patch: 0,
                        }
                    }
                ]],
            }
        );
    }

    #[test]
    fn test_check_requires() {
        // Have 7.0.4, require >= 8.
        let suricata_version = SuricataVersion::new(7, 0, 4);
        let requires = parse_requires("version >= 8").unwrap();
        assert_eq!(
            check_requires(&requires, &suricata_version),
            Err(RequiresError::VersionLt(SuricataVersion {
                major: 8,
                minor: 0,
                patch: 0,
            })),
        );

        // Have 7.0.4, require 7.0.3.
        let suricata_version = SuricataVersion::new(7, 0, 4);
        let requires = parse_requires("version >= 7.0.3").unwrap();
        assert_eq!(check_requires(&requires, &suricata_version), Ok(()));

        // Have 8.0.0, require >= 7.0.0 and < 8.0
        let suricata_version = SuricataVersion::new(8, 0, 0);
        let requires = parse_requires("version >= 7.0.0 < 8").unwrap();
        assert_eq!(
            check_requires(&requires, &suricata_version),
            Err(RequiresError::VersionGt)
        );

        // Have 8.0.0, require >= 7.0.0 and < 9.0
        let suricata_version = SuricataVersion::new(8, 0, 0);
        let requires = parse_requires("version >= 7.0.0 < 9").unwrap();
        assert_eq!(check_requires(&requires, &suricata_version), Ok(()));

        // Require feature foobar.
        let suricata_version = SuricataVersion::new(8, 0, 0);
        let requires = parse_requires("feature foobar").unwrap();
        assert_eq!(
            check_requires(&requires, &suricata_version),
            Err(RequiresError::MissingFeature("foobar".to_string()))
        );

        // Require feature foobar, but this time we have the feature.
        let suricata_version = SuricataVersion::new(8, 0, 0);
        let requires = parse_requires("feature true_foobar").unwrap();
        assert_eq!(check_requires(&requires, &suricata_version), Ok(()));

        let suricata_version = SuricataVersion::new(8, 0, 1);
        let requires = parse_requires("version >= 7.0.3 < 8").unwrap();
        assert!(check_requires(&requires, &suricata_version).is_err());

        let suricata_version = SuricataVersion::new(7, 0, 1);
        let requires = parse_requires("version >= 7.0.3 < 8").unwrap();
        assert!(check_requires(&requires, &suricata_version).is_err());

        let suricata_version = SuricataVersion::new(7, 0, 3);
        let requires = parse_requires("version >= 7.0.3 < 8").unwrap();
        assert!(check_requires(&requires, &suricata_version).is_ok());

        let suricata_version = SuricataVersion::new(8, 0, 3);
        let requires = parse_requires("version >= 7.0.3 < 8 | >= 8.0.3").unwrap();
        assert!(check_requires(&requires, &suricata_version).is_ok());

        let suricata_version = SuricataVersion::new(8, 0, 2);
        let requires = parse_requires("version >= 7.0.3 < 8 | >= 8.0.3").unwrap();
        assert!(check_requires(&requires, &suricata_version).is_err());

        let suricata_version = SuricataVersion::new(7, 0, 2);
        let requires = parse_requires("version >= 7.0.3 < 8 | >= 8.0.3").unwrap();
        assert!(check_requires(&requires, &suricata_version).is_err());

        let suricata_version = SuricataVersion::new(7, 0, 3);
        let requires = parse_requires("version >= 7.0.3 < 8 | >= 8.0.3").unwrap();
        assert!(check_requires(&requires, &suricata_version).is_ok());

        // Example of something that requires a fix/feature that was
        // implemented in 7.0.5, 8.0.4, 9.0.3.
        let requires = parse_requires("version >= 7.0.5 < 8 | >= 8.0.4 < 9 | >= 9.0.3").unwrap();
        assert!(check_requires(&requires, &SuricataVersion::new(6, 0, 0)).is_err());
        assert!(check_requires(&requires, &SuricataVersion::new(7, 0, 4)).is_err());
        assert!(check_requires(&requires, &SuricataVersion::new(7, 0, 5)).is_ok());
        assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 3)).is_err());
        assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 4)).is_ok());
        assert!(check_requires(&requires, &SuricataVersion::new(9, 0, 2)).is_err());
        assert!(check_requires(&requires, &SuricataVersion::new(9, 0, 3)).is_ok());
        assert!(check_requires(&requires, &SuricataVersion::new(10, 0, 0)).is_ok());

        let requires = parse_requires("version >= 8 < 9").unwrap();
        assert!(check_requires(&requires, &SuricataVersion::new(6, 0, 0)).is_err());
        assert!(check_requires(&requires, &SuricataVersion::new(7, 0, 0)).is_err());
        assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 0)).is_ok());
        assert!(check_requires(&requires, &SuricataVersion::new(9, 0, 0)).is_err());

        // Unknown keyword.
        let requires = parse_requires("feature lua, foo bar, version >= 7.0.3").unwrap();
        assert_eq!(
            requires,
            Requires {
                features: vec!["lua".to_string()],
                version: vec![vec![RuleRequireVersion {
                    op: VersionCompareOp::Gte,
                    version: SuricataVersion {
                        major: 7,
                        minor: 0,
                        patch: 3,
                    }
                }]],
            }
        );
    }

    #[test]
    fn test_parse_version_expression() {
        let version_str = ">= 7.0.3 < 8 | >= 8.0.3";
        let (rest, versions) = parse_version_expression(version_str).unwrap();
        assert!(rest.is_empty());
        assert_eq!(
            versions,
            vec![
                vec![
                    RuleRequireVersion {
                        op: VersionCompareOp::Gte,
                        version: SuricataVersion {
                            major: 7,
                            minor: 0,
                            patch: 3,
                        }
                    },
                    RuleRequireVersion {
                        op: VersionCompareOp::Lt,
                        version: SuricataVersion {
                            major: 8,
                            minor: 0,
                            patch: 0,
                        }
                    },
                ],
                vec![RuleRequireVersion {
                    op: VersionCompareOp::Gte,
                    version: SuricataVersion {
                        major: 8,
                        minor: 0,
                        patch: 3,
                    }
                },],
            ]
        );
    }
}
