1use crate::compiler::prelude::*;
2use cidr::IpCidr;
3use std::net::IpAddr;
4use std::str::FromStr;
5
6fn str_to_cidr(v: &str) -> Result<IpCidr, String> {
7 IpCidr::from_str(v).map_err(|err| format!("unable to parse CIDR: {err}"))
8}
9
10#[allow(clippy::result_large_err)]
11fn value_to_cidr(value: &Value) -> Result<IpCidr, function::Error> {
12 let str = &value.as_str().ok_or(function::Error::InvalidArgument {
13 keyword: "ip_cidr_contains",
14 value: value.clone(),
15 error: r#""cidr" must be string"#,
16 })?;
17
18 str_to_cidr(str).map_err(|_| function::Error::InvalidArgument {
19 keyword: "ip_cidr_contains",
20 value: value.clone(),
21 error: r#""cidr" must be valid cidr"#,
22 })
23}
24
25fn ip_cidr_contains(value: &Value, cidr: &Value) -> Resolved {
26 let bytes = value.try_bytes_utf8_lossy()?;
27 let ip_addr =
28 IpAddr::from_str(&bytes).map_err(|err| format!("unable to parse IP address: {err}"))?;
29
30 match cidr {
31 Value::Bytes(v) => {
32 let cidr = str_to_cidr(&String::from_utf8_lossy(v))?;
33 Ok(cidr.contains(&ip_addr).into())
34 }
35 Value::Array(vec) => {
36 for v in vec {
37 let cidr = str_to_cidr(&v.try_bytes_utf8_lossy()?)?;
38 if cidr.contains(&ip_addr) {
39 return Ok(true.into());
40 }
41 }
42 Ok(false.into())
43 }
44 value => Err(ValueError::Expected {
45 got: value.kind(),
46 expected: Kind::bytes() | Kind::array(Collection::any()),
47 }
48 .into()),
49 }
50}
51
52fn ip_cidr_contains_constant(value: &Value, cidr_vec: &[IpCidr]) -> Resolved {
53 let bytes = value.try_bytes_utf8_lossy()?;
54 let ip_addr =
55 IpAddr::from_str(&bytes).map_err(|err| format!("unable to parse IP address: {err}"))?;
56
57 Ok(cidr_vec.iter().any(|cidr| cidr.contains(&ip_addr)).into())
58}
59
60#[derive(Clone, Copy, Debug)]
61pub struct IpCidrContains;
62
63impl Function for IpCidrContains {
64 fn identifier(&self) -> &'static str {
65 "ip_cidr_contains"
66 }
67
68 fn usage(&self) -> &'static str {
69 "Determines whether the `ip` is contained in the block referenced by the `cidr`."
70 }
71
72 fn category(&self) -> &'static str {
73 Category::Ip.as_ref()
74 }
75
76 fn internal_failure_reasons(&self) -> &'static [&'static str] {
77 &[
78 "`cidr` is not a valid CIDR.",
79 "`ip` is not a valid IP address.",
80 ]
81 }
82
83 fn return_kind(&self) -> u16 {
84 kind::BOOLEAN
85 }
86
87 fn parameters(&self) -> &'static [Parameter] {
88 const PARAMETERS: &[Parameter] = &[
89 Parameter::required(
90 "cidr",
91 kind::BYTES | kind::ARRAY,
92 "The CIDR mask (v4 or v6).",
93 ),
94 Parameter::required("value", kind::BYTES, "The IP address (v4 or v6)."),
95 ];
96 PARAMETERS
97 }
98
99 fn examples(&self) -> &'static [Example] {
100 &[
101 example! {
102 title: "IPv4 contains CIDR",
103 source: r#"ip_cidr_contains!("192.168.0.0/16", "192.168.10.32")"#,
104 result: Ok("true"),
105 },
106 example! {
107 title: "IPv4 is private",
108 source: r#"ip_cidr_contains!(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], "192.168.10.32")"#,
109 result: Ok("true"),
110 },
111 example! {
112 title: "IPv6 contains CIDR",
113 source: r#"ip_cidr_contains!("2001:4f8:4:ba::/64", "2001:4f8:4:ba:2e0:81ff:fe22:d1f1")"#,
114 result: Ok("true"),
115 },
116 example! {
117 title: "Not in range",
118 source: r#"ip_cidr_contains!("192.168.0.0/24", "192.168.10.32")"#,
119 result: Ok("false"),
120 },
121 example! {
122 title: "Invalid address",
123 source: r#"ip_cidr_contains!("192.168.0.0/24", "INVALID")"#,
124 result: Err(
125 r#"function call error for "ip_cidr_contains" at (0:46): unable to parse IP address: invalid IP address syntax"#,
126 ),
127 },
128 ]
129 }
130
131 fn compile(
132 &self,
133 state: &state::TypeState,
134 _ctx: &mut FunctionCompileContext,
135 arguments: ArgumentList,
136 ) -> Compiled {
137 let cidr = arguments.required("cidr");
138
139 let cidr = match cidr.resolve_constant(state) {
140 None => IpCidrType::Expression(cidr),
141 Some(value) => IpCidrType::Constant(match value {
142 Value::Bytes(_) => vec![value_to_cidr(&value)?],
143 Value::Array(vec) => {
144 let mut output = Vec::with_capacity(vec.len());
145 for value in vec {
146 output.push(value_to_cidr(&value)?);
147 }
148 output
149 }
150 _ => {
151 return Err(function::Error::InvalidArgument {
152 keyword: "ip_cidr_contains",
153 value,
154 error: r#""cidr" must be string or array of strings"#,
155 }
156 .into());
157 }
158 }),
159 };
160
161 let value = arguments.required("value");
162
163 Ok(IpCidrContainsFn { cidr, value }.as_expr())
164 }
165}
166
167#[derive(Debug, Clone)]
168enum IpCidrType {
169 Constant(Vec<IpCidr>),
170 Expression(Box<dyn Expression>),
171}
172
173#[derive(Debug, Clone)]
174struct IpCidrContainsFn {
175 cidr: IpCidrType,
176 value: Box<dyn Expression>,
177}
178
179impl FunctionExpression for IpCidrContainsFn {
180 fn resolve(&self, ctx: &mut Context) -> Resolved {
181 let value = self.value.resolve(ctx)?;
182
183 match &self.cidr {
184 IpCidrType::Constant(cidr_vec) => ip_cidr_contains_constant(&value, cidr_vec),
185 IpCidrType::Expression(exp) => {
186 let cidr = exp.resolve(ctx)?;
187 ip_cidr_contains(&value, &cidr)
188 }
189 }
190 }
191
192 fn type_def(&self, _: &state::TypeState) -> TypeDef {
193 TypeDef::boolean().fallible()
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::value;
201
202 test_function! [
203 ip_cidr_contains => IpCidrContains;
204
205 ipv4_yes {
206 args: func_args![value: "192.168.10.32",
207 cidr: "192.168.0.0/16",
208 ],
209 want: Ok(value!(true)),
210 tdef: TypeDef::boolean().fallible(),
211 }
212
213 ipv4_no {
214 args: func_args![value: "192.168.10.32",
215 cidr: "192.168.0.0/24",
216 ],
217 want: Ok(value!(false)),
218 tdef: TypeDef::boolean().fallible(),
219 }
220
221 ipv4_yes_array {
222 args: func_args![value: "192.168.10.32",
223 cidr: vec!["10.0.0.0/8", "192.168.0.0/16"],
224 ],
225 want: Ok(value!(true)),
226 tdef: TypeDef::boolean().fallible(),
227 }
228
229 ipv4_no_array {
230 args: func_args![value: "192.168.10.32",
231 cidr: vec!["10.0.0.0/8", "192.168.0.0/24"],
232 ],
233 want: Ok(value!(false)),
234 tdef: TypeDef::boolean().fallible(),
235 }
236
237 ipv6_yes {
238 args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
239 cidr: "2001:4f8:3:ba::/64",
240 ],
241 want: Ok(value!(true)),
242 tdef: TypeDef::boolean().fallible(),
243 }
244
245 ipv6_no {
246 args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
247 cidr: "2001:4f8:4:ba::/64",
248 ],
249 want: Ok(value!(false)),
250 tdef: TypeDef::boolean().fallible(),
251 }
252
253 ipv6_yes_array {
254 args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
255 cidr: vec!["fc00::/7", "2001:4f8:3:ba::/64"],
256 ],
257 want: Ok(value!(true)),
258 tdef: TypeDef::boolean().fallible(),
259 }
260
261 ipv6_no_array {
262 args: func_args![value: "2001:4f8:3:ba:2e0:81ff:fe22:d1f1",
263 cidr: vec!["fc00::/7", "2001:4f8:4:ba::/64"],
264 ],
265 want: Ok(value!(false)),
266 tdef: TypeDef::boolean().fallible(),
267 }
268 ];
269}