vrl 0.32.0

Vector Remap Language
Documentation
use crate::compiler::prelude::*;
use cidr::IpCidr;
use std::net::IpAddr;
use std::str::FromStr;

fn str_to_cidr(v: &str) -> Result<IpCidr, String> {
    IpCidr::from_str(v).map_err(|err| format!("unable to parse CIDR: {err}"))
}

#[allow(clippy::result_large_err)]
fn value_to_cidr(value: &Value) -> Result<IpCidr, function::Error> {
    let str = &value.as_str().ok_or(function::Error::InvalidArgument {
        keyword: "ip_cidr_contains",
        value: value.clone(),
        error: r#""cidr" must be string"#,
    })?;

    str_to_cidr(str).map_err(|_| function::Error::InvalidArgument {
        keyword: "ip_cidr_contains",
        value: value.clone(),
        error: r#""cidr" must be valid cidr"#,
    })
}

fn ip_cidr_contains(value: &Value, cidr: &Value) -> Resolved {
    let bytes = value.try_bytes_utf8_lossy()?;
    let ip_addr =
        IpAddr::from_str(&bytes).map_err(|err| format!("unable to parse IP address: {err}"))?;

    match cidr {
        Value::Bytes(v) => {
            let cidr = str_to_cidr(&String::from_utf8_lossy(v))?;
            Ok(cidr.contains(&ip_addr).into())
        }
        Value::Array(vec) => {
            for v in vec {
                let cidr = str_to_cidr(&v.try_bytes_utf8_lossy()?)?;
                if cidr.contains(&ip_addr) {
                    return Ok(true.into());
                }
            }
            Ok(false.into())
        }
        value => Err(ValueError::Expected {
            got: value.kind(),
            expected: Kind::bytes() | Kind::array(Collection::any()),
        }
        .into()),
    }
}

fn ip_cidr_contains_constant(value: &Value, cidr_vec: &[IpCidr]) -> Resolved {
    let bytes = value.try_bytes_utf8_lossy()?;
    let ip_addr =
        IpAddr::from_str(&bytes).map_err(|err| format!("unable to parse IP address: {err}"))?;

    Ok(cidr_vec.iter().any(|cidr| cidr.contains(&ip_addr)).into())
}

#[derive(Clone, Copy, Debug)]
pub struct IpCidrContains;

impl Function for IpCidrContains {
    fn identifier(&self) -> &'static str {
        "ip_cidr_contains"
    }

    fn usage(&self) -> &'static str {
        "Determines whether the `ip` is contained in the block referenced by the `cidr`."
    }

    fn category(&self) -> &'static str {
        Category::Ip.as_ref()
    }

    fn internal_failure_reasons(&self) -> &'static [&'static str] {
        &[
            "`cidr` is not a valid CIDR.",
            "`ip` is not a valid IP address.",
        ]
    }

    fn return_kind(&self) -> u16 {
        kind::BOOLEAN
    }

    fn parameters(&self) -> &'static [Parameter] {
        const PARAMETERS: &[Parameter] = &[
            Parameter::required(
                "cidr",
                kind::BYTES | kind::ARRAY,
                "The CIDR mask (v4 or v6).",
            ),
            Parameter::required("value", kind::BYTES, "The IP address (v4 or v6)."),
        ];
        PARAMETERS
    }

    fn examples(&self) -> &'static [Example] {
        &[
            example! {
                title: "IPv4 contains CIDR",
                source: r#"ip_cidr_contains!("192.168.0.0/16", "192.168.10.32")"#,
                result: Ok("true"),
            },
            example! {
                title: "IPv4 is private",
                source: r#"ip_cidr_contains!(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], "192.168.10.32")"#,
                result: Ok("true"),
            },
            example! {
                title: "IPv6 contains CIDR",
                source: r#"ip_cidr_contains!("2001:4f8:4:ba::/64", "2001:4f8:4:ba:2e0:81ff:fe22:d1f1")"#,
                result: Ok("true"),
            },
            example! {
                title: "Not in range",
                source: r#"ip_cidr_contains!("192.168.0.0/24", "192.168.10.32")"#,
                result: Ok("false"),
            },
            example! {
                title: "Invalid address",
                source: r#"ip_cidr_contains!("192.168.0.0/24", "INVALID")"#,
                result: Err(
                    r#"function call error for "ip_cidr_contains" at (0:46): unable to parse IP address: invalid IP address syntax"#,
                ),
            },
        ]
    }

    fn compile(
        &self,
        state: &state::TypeState,
        _ctx: &mut FunctionCompileContext,
        arguments: ArgumentList,
    ) -> Compiled {
        let cidr = arguments.required("cidr");

        let cidr = match cidr.resolve_constant(state) {
            None => IpCidrType::Expression(cidr),
            Some(value) => IpCidrType::Constant(match value {
                Value::Bytes(_) => vec![value_to_cidr(&value)?],
                Value::Array(vec) => {
                    let mut output = Vec::with_capacity(vec.len());
                    for value in vec {
                        output.push(value_to_cidr(&value)?);
                    }
                    output
                }
                _ => {
                    return Err(function::Error::InvalidArgument {
                        keyword: "ip_cidr_contains",
                        value,
                        error: r#""cidr" must be string or array of strings"#,
                    }
                    .into());
                }
            }),
        };

        let value = arguments.required("value");

        Ok(IpCidrContainsFn { cidr, value }.as_expr())
    }
}

#[derive(Debug, Clone)]
enum IpCidrType {
    Constant(Vec<IpCidr>),
    Expression(Box<dyn Expression>),
}

#[derive(Debug, Clone)]
struct IpCidrContainsFn {
    cidr: IpCidrType,
    value: Box<dyn Expression>,
}

impl FunctionExpression for IpCidrContainsFn {
    fn resolve(&self, ctx: &mut Context) -> Resolved {
        let value = self.value.resolve(ctx)?;

        match &self.cidr {
            IpCidrType::Constant(cidr_vec) => ip_cidr_contains_constant(&value, cidr_vec),
            IpCidrType::Expression(exp) => {
                let cidr = exp.resolve(ctx)?;
                ip_cidr_contains(&value, &cidr)
            }
        }
    }

    fn type_def(&self, _: &state::TypeState) -> TypeDef {
        TypeDef::boolean().fallible()
    }
}

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

    test_function! [
        ip_cidr_contains => IpCidrContains;

        ipv4_yes {
            args: func_args![value: "192.168.10.32",
                             cidr: "192.168.0.0/16",
            ],
            want: Ok(value!(true)),
            tdef: TypeDef::boolean().fallible(),
        }

        ipv4_no {
            args: func_args![value: "192.168.10.32",
                             cidr: "192.168.0.0/24",
            ],
            want: Ok(value!(false)),
            tdef: TypeDef::boolean().fallible(),
        }

        ipv4_yes_array {
            args: func_args![value: "192.168.10.32",
                             cidr: vec!["10.0.0.0/8", "192.168.0.0/16"],
            ],
            want: Ok(value!(true)),
            tdef: TypeDef::boolean().fallible(),
        }

        ipv4_no_array {
            args: func_args![value: "192.168.10.32",
                             cidr: vec!["10.0.0.0/8", "192.168.0.0/24"],
            ],
            want: Ok(value!(false)),
            tdef: TypeDef::boolean().fallible(),
        }

        ipv6_yes {
            args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
                             cidr: "2001:4f8:3:ba::/64",
            ],
            want: Ok(value!(true)),
            tdef: TypeDef::boolean().fallible(),
        }

        ipv6_no {
            args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
                             cidr: "2001:4f8:4:ba::/64",
            ],
            want: Ok(value!(false)),
            tdef: TypeDef::boolean().fallible(),
        }

        ipv6_yes_array {
            args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
                             cidr: vec!["fc00::/7", "2001:4f8:3:ba::/64"],
            ],
            want: Ok(value!(true)),
            tdef: TypeDef::boolean().fallible(),
        }

        ipv6_no_array {
            args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
                             cidr: vec!["fc00::/7", "2001:4f8:4:ba::/64"],
            ],
            want: Ok(value!(false)),
            tdef: TypeDef::boolean().fallible(),
        }
    ];
}