cedar_policy_core/extensions/
ipaddr.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! This module contains the Cedar 'ipaddr' extension.
18
19use crate::ast::{
20    CallStyle, Extension, ExtensionFunction, ExtensionOutputValue, ExtensionValue, Literal, Name,
21    RepresentableExtensionValue, Type, Value, ValueKind,
22};
23use crate::entities::SchemaType;
24use crate::evaluator;
25use std::sync::Arc;
26
27// PANIC SAFETY All the names are valid names
28#[allow(clippy::expect_used)]
29mod names {
30    use crate::ast::Name;
31    lazy_static::lazy_static! {
32        pub static ref EXTENSION_NAME : Name = Name::parse_unqualified_name("ipaddr").expect("should be a valid identifier");
33        pub static ref IP_FROM_STR_NAME : Name = Name::parse_unqualified_name("ip").expect("should be a valid identifier");
34        pub static ref IS_IPV4 : Name = Name::parse_unqualified_name("isIpv4").expect("should be a valid identifier");
35        pub static ref IS_IPV6 : Name = Name::parse_unqualified_name("isIpv6").expect("should be a valid identifier");
36        pub static ref IS_LOOPBACK : Name = Name::parse_unqualified_name("isLoopback").expect("should be a valid identifier");
37        pub static ref IS_MULTICAST : Name = Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier");
38        pub static ref IS_IN_RANGE : Name = Name::parse_unqualified_name("isInRange").expect("should be a valid identifier");
39    }
40}
41
42/// Help message to display when a String was provided where an IP value was expected.
43/// This error is likely due to confusion between "127.0.0.1" and ip("127.0.0.1").
44const ADVICE_MSG: &str = "maybe you forgot to apply the `ip` constructor?";
45
46/// Maximum prefix size for IpV4 addresses
47const PREFIX_MAX_LEN_V4: u8 = 32;
48/// Maximum prefix size for IpV6 addresses
49const PREFIX_MAX_LEN_V6: u8 = 128;
50/// Maximum prefix string size for IpV4 addresses
51/// len('32') = 2
52const PREFIX_STR_MAX_LEN_V4: u8 = 2;
53/// Maximum prefix string size for IpV6 addresses
54/// len('128') = 3
55const PREFIX_STR_MAX_LEN_V6: u8 = 3;
56/// The maximum length of an IpNet in bytes is
57/// len('ABCD:EF01:2345:6789:ABCD:EF01:2345:6789/128') = 43
58const IP_STR_REP_MAX_LEN: u8 = 43;
59
60#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
61struct IPAddr {
62    /// the actual address, without prefix
63    addr: std::net::IpAddr,
64    /// Prefix -- the part after the `/` in CIDR.
65    /// A single address will have `32` here (in the IPv4 case) or `128` (in the IPv6 case).
66    prefix: u8,
67}
68
69impl IPAddr {
70    /// The Cedar typename of all ipaddr values
71    fn typename() -> Name {
72        names::EXTENSION_NAME.clone()
73    }
74
75    /// Convert an IP address or subnet, given as a string, into an `IPAddr`
76    /// value.
77    ///
78    /// This accepts both IPv4 and IPv6 addresses, in their standard text
79    /// formats. It also accepts both single addresses (like `"10.1.1.0"`) and
80    /// subnets (like `"10.1.1.0/24"`).
81    /// It does not accept IPv4 addresses embedded in IPv6 (e.g., `"::ffff:192.168.0.1"`).
82    /// These addresses can be written as hexadecimal IPv6. Comparisons between any IPv4 address
83    ///  and any IPv6 address (including an IPv4 address embedded in IPv6) is false (e.g.,
84    ///  `isLoopback("::ffff:ff00:1")` is `false`)
85    fn from_str(str: impl AsRef<str>) -> Result<Self, String> {
86        // Delegate to `FromStr` implementation
87        str.as_ref().parse()
88    }
89
90    /// Return true if this is an IPv4 address
91    fn is_ipv4(&self) -> bool {
92        self.addr.is_ipv4()
93    }
94
95    /// Return true if this is an IPv6 address
96    fn is_ipv6(&self) -> bool {
97        self.addr.is_ipv6()
98    }
99
100    /// Return true if this is a loopback address
101    fn is_loopback(&self) -> bool {
102        // Loopback addresses are "127.0.0.0/8" for IpV4 and "::1" for IpV6
103        // If `addr` is a loopback address, its prefix is `0x7f` or `0x00000000000000000000000000000001`
104        // We need to just make sure the prefix length (i.e., `prefix`) is greater than or equal to `8` or `128`
105        self.addr.is_loopback() && self.prefix >= if self.is_ipv4() { 8 } else { PREFIX_MAX_LEN_V6 }
106    }
107
108    /// Return true if this is a multicast address
109    fn is_multicast(&self) -> bool {
110        // Multicast addresses are "224.0.0.0/4" for IpV4 and "ff00::/8" for IpV6
111        // Following the same reasoning as `is_loopback`
112        self.addr.is_multicast() && self.prefix >= if self.is_ipv4() { 4 } else { 8 }
113    }
114
115    /// Return true if this is contained in the given `IPAddr`
116    fn is_in_range(&self, other: &Self) -> bool {
117        match (&self.addr, &other.addr) {
118            (std::net::IpAddr::V4(self_v4), std::net::IpAddr::V4(other_v4)) => {
119                let netmask = |prefix: u8| {
120                    u32::MAX
121                        .checked_shl((PREFIX_MAX_LEN_V4 - prefix).into())
122                        .unwrap_or(0)
123                };
124                let hostmask = |prefix: u8| u32::MAX.checked_shr(prefix.into()).unwrap_or(0);
125
126                let self_network = u32::from(*self_v4) & netmask(self.prefix);
127                let other_network = u32::from(*other_v4) & netmask(other.prefix);
128                let self_broadcast = u32::from(*self_v4) | hostmask(self.prefix);
129                let other_broadcast = u32::from(*other_v4) | hostmask(other.prefix);
130                other_network <= self_network && self_broadcast <= other_broadcast
131            }
132            (std::net::IpAddr::V6(self_v6), std::net::IpAddr::V6(other_v6)) => {
133                let netmask = |prefix: u8| {
134                    u128::MAX
135                        .checked_shl((PREFIX_MAX_LEN_V6 - prefix).into())
136                        .unwrap_or(0)
137                };
138                let hostmask = |prefix: u8| u128::MAX.checked_shr(prefix.into()).unwrap_or(0);
139
140                let self_network = u128::from(*self_v6) & netmask(self.prefix);
141                let other_network = u128::from(*other_v6) & netmask(other.prefix);
142                let self_broadcast = u128::from(*self_v6) | hostmask(self.prefix);
143                let other_broadcast = u128::from(*other_v6) | hostmask(other.prefix);
144                other_network <= self_network && self_broadcast <= other_broadcast
145            }
146            (_, _) => false,
147        }
148    }
149}
150
151fn parse_prefix(s: &str, max: u8, max_len: u8) -> Result<u8, String> {
152    if s.len() > max_len as usize {
153        return Err(format!(
154            "error parsing prefix: string length {} is too large",
155            s.len()
156        ));
157    }
158    if s.chars().any(|c| !c.is_ascii_digit()) {
159        return Err(format!("error parsing prefix `{s}`: encountered non-digit"));
160    }
161    if s.starts_with('0') && s != "0" {
162        return Err(format!("error parsing prefix `{s}`: leading zero(s)"));
163    }
164    let res: u8 = s
165        .parse()
166        .map_err(|err| format!("error parsing prefix from the string `{s}`: {err}"))?;
167    if res > max {
168        return Err(format!(
169            "error parsing prefix: {res} is larger than the limit {max}"
170        ));
171    }
172    Ok(res)
173}
174
175impl std::str::FromStr for IPAddr {
176    type Err = String;
177    fn from_str(s: &str) -> Result<Self, Self::Err> {
178        // Return Err if the input is too long
179        if s.bytes().len() > IP_STR_REP_MAX_LEN as usize {
180            return Err(format!(
181                "error parsing IP address from string `{s}`: string length is too large"
182            ));
183        }
184        // Return Err if string is IPv4 embedded in IPv6 format
185        str_contains_colons_and_dots(s)?;
186
187        // Split over '/' first so we don't have to parse the address field twice
188        match s.split_once('/') {
189            Some((addr_str, prefix_str)) => {
190                // `addr` (the part before the slash) should be a valid IP address,
191                // while `prefix` should be either 0..32 or 0..128 depending on the version
192                let addr: std::net::IpAddr = addr_str.parse().map_err(|e| {
193                    format!("error parsing IP address from the string `{addr_str}`: {e}")
194                })?;
195                let prefix = match addr {
196                    std::net::IpAddr::V4(_) => {
197                        parse_prefix(prefix_str, PREFIX_MAX_LEN_V4, PREFIX_STR_MAX_LEN_V4)?
198                    }
199                    std::net::IpAddr::V6(_) => {
200                        parse_prefix(prefix_str, PREFIX_MAX_LEN_V6, PREFIX_STR_MAX_LEN_V6)?
201                    }
202                };
203                Ok(Self { addr, prefix })
204            }
205            None => match std::net::IpAddr::from_str(s) {
206                Ok(singleaddr) => Ok(Self {
207                    addr: singleaddr,
208                    prefix: if singleaddr.is_ipv4() {
209                        PREFIX_MAX_LEN_V4
210                    } else {
211                        PREFIX_MAX_LEN_V6
212                    },
213                }),
214                Err(_) => Err(format!("invalid IP address: {s}")),
215            },
216        }
217    }
218}
219
220impl std::fmt::Display for IPAddr {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        write!(f, "{}/{}", self.addr, self.prefix)
223    }
224}
225
226impl ExtensionValue for IPAddr {
227    fn typename(&self) -> Name {
228        Self::typename()
229    }
230    fn supports_operator_overloading(&self) -> bool {
231        false
232    }
233}
234
235fn extension_err(msg: impl Into<String>) -> evaluator::EvaluationError {
236    evaluator::EvaluationError::failed_extension_function_application(
237        names::EXTENSION_NAME.clone(),
238        msg.into(),
239        None, // source loc will be added by the evaluator
240        None,
241    )
242}
243
244/// Check whether `s` contains at least three occurences of `c`
245fn contains_at_least_two(s: &str, c: char) -> bool {
246    let idx = s.find(c);
247    match idx {
248        Some(i) => {
249            // For this slicing operation not to panic, two preconditions must be met:
250            // 1) `i + c.len_utf()` must be <= `s.len()`
251            //     This is met because:
252            //     i := s.find(c) meaning:
253            //     A) `i` < `s.len()`
254            //     B) `c` is in the list at position `i`, meaning `c.utf_len()` is at most going to
255            //         point to the end of the string
256            // 2) `i + c.utf_len()` must be a character boundary
257            //     This is met because we use `c.len_utf()` which is guaranteed to be the length of
258            //     `c`, since Rust strings use UTF8 and `c` is guaranteed to be the character at
259            //     position `i` in `s`.
260            // The above is checked by [`proof::contains_at_least_two_correct`], which proves
261            // safety for strings up to length 6
262            // PANIC SAFETY: (see above)
263            #[allow(clippy::indexing_slicing)]
264            // PANIC SAFETY: (see above)
265            #[allow(clippy::unwrap_used)]
266            let idx = s.get(i + c.len_utf8()..).unwrap().find(c);
267            idx.is_some()
268        }
269        None => false,
270    }
271}
272
273#[cfg(kani)]
274mod proof {
275
276    /// Prove that `contains_two_correct` does not panic for strings with length <= 6
277    #[kani::proof]
278    #[kani::unwind(7)]
279    fn contains_at_least_two_correct() {
280        let buf: [u8; 6] = kani::any();
281        let len: usize = kani::any();
282        kani::assume(len <= 6);
283        let slice = &buf[0..len];
284        if let Ok(s) = std::str::from_utf8(slice) {
285            let pat = kani::any();
286            // Just don't panic
287            let _ = super::contains_at_least_two(s, pat);
288        }
289    }
290}
291
292/// To try to avoid confusion, we currently refuse to parse IPv4 embeded in IPv6.
293/// Specificially, we reject string reprsentations of IPv4-Compatible IPv6 addresses and IPv4-Mapped IPv6 addresses (https://doc.rust-lang.org/std/net/struct.Ipv6Addr.html#embedding-ipv4-addresses).
294/// These addresses mix colon and dot notation: (e.g., "::ffff:192.168.0.1" and "::127.0.0.1")
295/// We will, though, parse IPv4 embedded in IPv6 if it is provided in "normal" IPv6 format (e.g., "::ffff:ff00:1"). Such addresses are treated as IPv6 addresses
296/// These addresses must contain at least two colons and three periods. (We check for more than one to allow adding IPv4 addresses with ports in the future)
297/// To simplify the implementation, we reject addresses with at least two colons and two periods.
298fn str_contains_colons_and_dots(s: &str) -> Result<(), String> {
299    if contains_at_least_two(s, ':') && contains_at_least_two(s, '.') {
300        return Err(format!(
301            "error parsing IP address from string: We do not accept IPv4 embedded in IPv6 (e.g., ::ffff:127.0.0.1). Found: `{}`", &s.to_string()));
302    }
303    Ok(())
304}
305
306/// Cedar function which constructs an `ipaddr` Cedar type from a
307/// Cedar string
308fn ip_from_str(arg: &Value) -> evaluator::Result<ExtensionOutputValue> {
309    let str = arg.get_as_string()?;
310    let arg_source_loc = arg.source_loc().cloned();
311    let ipaddr = RepresentableExtensionValue::new(
312        Arc::new(IPAddr::from_str(str.as_str()).map_err(extension_err)?),
313        names::IP_FROM_STR_NAME.clone(),
314        vec![arg.clone().into()],
315    );
316    Ok(Value {
317        value: ValueKind::ExtensionValue(Arc::new(ipaddr)),
318        loc: arg_source_loc, // this gives the loc of the arg. We could perhaps give instead the loc of the entire `ip("...")` call, but that is hard to do at this program point
319    }
320    .into())
321}
322
323fn as_ipaddr(v: &Value) -> Result<&IPAddr, evaluator::EvaluationError> {
324    match &v.value {
325        ValueKind::ExtensionValue(ev) if ev.typename() == IPAddr::typename() => {
326            // PANIC SAFETY Conditional above performs a typecheck
327            #[allow(clippy::expect_used)]
328            let ipaddr = ev
329                .value()
330                .as_any()
331                .downcast_ref::<IPAddr>()
332                .expect("already typechecked, so this downcast should succeed");
333            Ok(ipaddr)
334        }
335        ValueKind::Lit(Literal::String(_)) => {
336            Err(evaluator::EvaluationError::type_error_with_advice_single(
337                Type::Extension {
338                    name: IPAddr::typename(),
339                },
340                v,
341                ADVICE_MSG.into(),
342            ))
343        }
344        _ => Err(evaluator::EvaluationError::type_error_single(
345            Type::Extension {
346                name: IPAddr::typename(),
347            },
348            v,
349        )),
350    }
351}
352
353/// Cedar function which tests whether an `ipaddr` Cedar type is an IPv4
354/// address, returning a Cedar bool
355fn is_ipv4(arg: &Value) -> evaluator::Result<ExtensionOutputValue> {
356    let ipaddr = as_ipaddr(arg)?;
357    Ok(ipaddr.is_ipv4().into())
358}
359
360/// Cedar function which tests whether an `ipaddr` Cedar type is an IPv6
361/// address, returning a Cedar bool
362fn is_ipv6(arg: &Value) -> evaluator::Result<ExtensionOutputValue> {
363    let ipaddr = as_ipaddr(arg)?;
364    Ok(ipaddr.is_ipv6().into())
365}
366
367/// Cedar function which tests whether an `ipaddr` Cedar type is a
368/// loopback address, returning a Cedar bool
369fn is_loopback(arg: &Value) -> evaluator::Result<ExtensionOutputValue> {
370    let ipaddr = as_ipaddr(arg)?;
371    Ok(ipaddr.is_loopback().into())
372}
373
374/// Cedar function which tests whether an `ipaddr` Cedar type is a
375/// multicast address, returning a Cedar bool
376fn is_multicast(arg: &Value) -> evaluator::Result<ExtensionOutputValue> {
377    let ipaddr = as_ipaddr(arg)?;
378    Ok(ipaddr.is_multicast().into())
379}
380
381/// Cedar function which tests whether the first `ipaddr` Cedar type is
382/// in the IP range represented by the second `ipaddr` Cedar type, returning
383/// a Cedar bool
384fn is_in_range(child: &Value, parent: &Value) -> evaluator::Result<ExtensionOutputValue> {
385    let child_ip = as_ipaddr(child)?;
386    let parent_ip = as_ipaddr(parent)?;
387    Ok(child_ip.is_in_range(parent_ip).into())
388}
389
390/// Construct the extension
391pub fn extension() -> Extension {
392    let ipaddr_type = SchemaType::Extension {
393        name: IPAddr::typename(),
394    };
395    Extension::new(
396        names::EXTENSION_NAME.clone(),
397        vec![
398            ExtensionFunction::unary(
399                names::IP_FROM_STR_NAME.clone(),
400                CallStyle::FunctionStyle,
401                Box::new(ip_from_str),
402                ipaddr_type.clone(),
403                SchemaType::String,
404            ),
405            ExtensionFunction::unary(
406                names::IS_IPV4.clone(),
407                CallStyle::MethodStyle,
408                Box::new(is_ipv4),
409                SchemaType::Bool,
410                ipaddr_type.clone(),
411            ),
412            ExtensionFunction::unary(
413                names::IS_IPV6.clone(),
414                CallStyle::MethodStyle,
415                Box::new(is_ipv6),
416                SchemaType::Bool,
417                ipaddr_type.clone(),
418            ),
419            ExtensionFunction::unary(
420                names::IS_LOOPBACK.clone(),
421                CallStyle::MethodStyle,
422                Box::new(is_loopback),
423                SchemaType::Bool,
424                ipaddr_type.clone(),
425            ),
426            ExtensionFunction::unary(
427                names::IS_MULTICAST.clone(),
428                CallStyle::MethodStyle,
429                Box::new(is_multicast),
430                SchemaType::Bool,
431                ipaddr_type.clone(),
432            ),
433            ExtensionFunction::binary(
434                names::IS_IN_RANGE.clone(),
435                CallStyle::MethodStyle,
436                Box::new(is_in_range),
437                SchemaType::Bool,
438                (ipaddr_type.clone(), ipaddr_type),
439            ),
440        ],
441        std::iter::empty(),
442    )
443}
444
445// PANIC SAFETY: Unit Test Code
446#[allow(clippy::panic)]
447#[cfg(test)]
448#[allow(clippy::cognitive_complexity)]
449mod tests {
450    use super::*;
451    use crate::ast::{Expr, Type, Value};
452    use crate::evaluator::test::{basic_entities, basic_request};
453    use crate::evaluator::{evaluation_errors, EvaluationError, Evaluator};
454    use crate::extensions::Extensions;
455    use crate::parser::parse_expr;
456    use cool_asserts::assert_matches;
457    use nonempty::nonempty;
458
459    /// This helper function asserts that a `Result` is actually an
460    /// `Err::ExtensionErr` with our extension name
461    #[track_caller] // report the caller's location as the location of the panic, not the location in this function
462    fn assert_ipaddr_err<T: std::fmt::Debug>(res: evaluator::Result<T>) {
463        assert_matches!(res, Err(EvaluationError::FailedExtensionFunctionExecution(evaluation_errors::ExtensionFunctionExecutionError { extension_name, .. })) => {
464            assert_eq!(
465                extension_name,
466                Name::parse_unqualified_name("ipaddr")
467                    .expect("should be a valid identifier")
468            );
469        });
470    }
471
472    /// This helper function returns an `Expr` that calls `ip()` with the given single argument
473    fn ip(arg: impl Into<Literal>) -> Expr {
474        Expr::call_extension_fn(
475            Name::parse_unqualified_name("ip").expect("should be a valid identifier"),
476            vec![Expr::val(arg)],
477        )
478    }
479
480    /// this test just ensures that the right functions are marked constructors
481    #[test]
482    fn constructors() {
483        let ext = extension();
484        assert!(ext
485            .get_func(&Name::parse_unqualified_name("ip").expect("should be a valid identifier"))
486            .expect("function should exist")
487            .is_constructor());
488        assert!(!ext
489            .get_func(
490                &Name::parse_unqualified_name("isIpv4").expect("should be a valid identifier")
491            )
492            .expect("function should exist")
493            .is_constructor());
494        assert!(!ext
495            .get_func(
496                &Name::parse_unqualified_name("isIpv6").expect("should be a valid identifier")
497            )
498            .expect("function should exist")
499            .is_constructor());
500        assert!(!ext
501            .get_func(
502                &Name::parse_unqualified_name("isLoopback").expect("should be a valid identifier")
503            )
504            .expect("function should exist")
505            .is_constructor());
506        assert!(!ext
507            .get_func(
508                &Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier")
509            )
510            .expect("function should exist")
511            .is_constructor());
512        assert!(!ext
513            .get_func(
514                &Name::parse_unqualified_name("isInRange").expect("should be a valid identifier")
515            )
516            .expect("function should exist")
517            .is_constructor(),);
518    }
519
520    #[test]
521    fn ip_creation() {
522        let ext_array = [extension()];
523        let exts = Extensions::specific_extensions(&ext_array).unwrap();
524        let request = basic_request();
525        let entities = basic_entities();
526        let eval = Evaluator::new(request, &entities, &exts);
527
528        // test that normal stuff still works with ipaddr extension enabled
529        assert_eq!(
530            eval.interpret_inline_policy(
531                &parse_expr(r#""pancakes" like "pan*""#).expect("parsing error")
532            ),
533            Ok(Value::from(true))
534        );
535
536        // test that an ipv4 address parses from string and isIpv4 but not isIpv6
537        assert_eq!(
538            eval.interpret_inline_policy(&Expr::call_extension_fn(
539                Name::parse_unqualified_name("isIpv4").expect("should be a valid identifier"),
540                vec![ip("127.0.0.1")]
541            )),
542            Ok(Value::from(true))
543        );
544        assert_eq!(
545            eval.interpret_inline_policy(&Expr::call_extension_fn(
546                Name::parse_unqualified_name("isIpv6").expect("should be a valid identifier"),
547                vec![ip("127.0.0.1")]
548            )),
549            Ok(Value::from(false))
550        );
551
552        // test that an ipv6 address parses from string and isIpv6 but not isIpv4
553        assert_eq!(
554            eval.interpret_inline_policy(&Expr::call_extension_fn(
555                Name::parse_unqualified_name("isIpv4").expect("should be a valid identifier"),
556                vec![ip("::1")]
557            )),
558            Ok(Value::from(false))
559        );
560        assert_eq!(
561            eval.interpret_inline_policy(&Expr::call_extension_fn(
562                Name::parse_unqualified_name("isIpv6").expect("should be a valid identifier"),
563                vec![ip("::1")]
564            )),
565            Ok(Value::from(true))
566        );
567
568        // test that parsing hexadecimal IPv4 embeded in IPv6 address parses from string and isIpv6 but not isIpv4
569        assert_eq!(
570            eval.interpret_inline_policy(&Expr::call_extension_fn(
571                Name::parse_unqualified_name("isIpv4").expect("should be a valid identifier"),
572                vec![ip("::ffff:ff00:1")]
573            )),
574            Ok(Value::from(false))
575        );
576        assert_eq!(
577            eval.interpret_inline_policy(&Expr::call_extension_fn(
578                Name::parse_unqualified_name("isIpv6").expect("should be a valid identifier"),
579                vec![ip("::ffff:ff00:1")]
580            )),
581            Ok(Value::from(true))
582        );
583
584        // test for parse errors when parsing from string
585        assert_ipaddr_err(eval.interpret_inline_policy(&ip("380.0.0.1")));
586        assert_ipaddr_err(eval.interpret_inline_policy(&ip("?")));
587        assert_ipaddr_err(eval.interpret_inline_policy(&ip("ab.ab.ab.ab")));
588        assert_ipaddr_err(eval.interpret_inline_policy(&ip("foo::1")));
589        //Test parsing IPv4 embedded in IPv6 is an error
590        assert_ipaddr_err(eval.interpret_inline_policy(&ip("::ffff:127.0.0.1")));
591        assert_ipaddr_err(eval.interpret_inline_policy(&ip("::127.0.0.1")));
592        assert_matches!(
593            eval.interpret_inline_policy(&Expr::call_extension_fn(
594                Name::parse_unqualified_name("ip").expect("should be a valid identifier"),
595                vec![Expr::set(vec![
596                    Expr::val(127),
597                    Expr::val(0),
598                    Expr::val(0),
599                    Expr::val(1)
600                ])]
601            )),
602            Err(EvaluationError::TypeError(evaluation_errors::TypeError { expected, actual, advice, .. })) => {
603                assert_eq!(expected, nonempty![Type::String]);
604                assert_eq!(actual, Type::Set);
605                assert_eq!(advice, None);
606            }
607        );
608
609        // test that < on ipaddr values is an error
610        assert_matches!(
611            eval.interpret_inline_policy(&Expr::less(ip("127.0.0.1"), ip("10.0.0.10"))),
612            Err(EvaluationError::TypeError(evaluation_errors::TypeError { expected, actual, advice, .. })) => {
613                assert_eq!(expected, nonempty![Type::Long]);
614                assert_eq!(actual, Type::Extension {
615                    name: Name::parse_unqualified_name("ipaddr")
616                        .expect("should be a valid identifier")
617                });
618                assert_eq!(advice, Some("Only types long support comparison".into()));
619            }
620        );
621        // test that isIpv4 on a String is an error
622        assert_matches!(
623            eval.interpret_inline_policy(&Expr::call_extension_fn(
624                Name::parse_unqualified_name("isIpv4").expect("should be a valid identifier"),
625                vec![Expr::val("127.0.0.1")]
626            )),
627            Err(EvaluationError::TypeError(evaluation_errors::TypeError { expected, actual, advice, .. })) => {
628                assert_eq!(expected, nonempty![Type::Extension {
629                    name: Name::parse_unqualified_name("ipaddr")
630                        .expect("should be a valid identifier")
631                }]);
632                assert_eq!(actual, Type::String);
633                assert_eq!(advice, Some(ADVICE_MSG.into()));
634            }
635        );
636
637        // test the Display impl
638        assert_eq!(
639            eval.interpret_inline_policy(&ip("127.0.0.1"))
640                .unwrap()
641                .to_string(),
642            r#"ip("127.0.0.1")"#
643        );
644        assert_eq!(
645            eval.interpret_inline_policy(&ip("ffee::11"))
646                .unwrap()
647                .to_string(),
648            r#"ip("ffee::11")"#
649        );
650    }
651
652    #[test]
653    fn ip_range_creation() {
654        let ext_array = [extension()];
655        let exts = Extensions::specific_extensions(&ext_array).unwrap();
656        let request = basic_request();
657        let entities = basic_entities();
658        let eval = Evaluator::new(request, &entities, &exts);
659
660        // test that an ipv4 range parses from string and isIpv4 but not isIpv6
661        assert_eq!(
662            eval.interpret_inline_policy(&Expr::call_extension_fn(
663                Name::parse_unqualified_name("isIpv4").expect("should be a valid identifier"),
664                vec![ip("127.0.0.1/24")]
665            )),
666            Ok(Value::from(true))
667        );
668        assert_eq!(
669            eval.interpret_inline_policy(&Expr::call_extension_fn(
670                Name::parse_unqualified_name("isIpv6").expect("should be a valid identifier"),
671                vec![ip("127.0.0.1/24")]
672            )),
673            Ok(Value::from(false))
674        );
675
676        // test that an ipv6 range parses from string and isIpv6 but not isIpv4
677        assert_eq!(
678            eval.interpret_inline_policy(&Expr::call_extension_fn(
679                Name::parse_unqualified_name("isIpv4").expect("should be a valid identifier"),
680                vec![ip("ffee::/64")]
681            )),
682            Ok(Value::from(false))
683        );
684        assert_eq!(
685            eval.interpret_inline_policy(&Expr::call_extension_fn(
686                Name::parse_unqualified_name("isIpv6").expect("should be a valid identifier"),
687                vec![ip("ffee::/64")]
688            )),
689            Ok(Value::from(true))
690        );
691
692        // test the extremes of valid values for prefix
693        assert_matches!(eval.interpret_inline_policy(&ip("127.0.0.1/0")), Ok(_));
694        assert_matches!(eval.interpret_inline_policy(&ip("127.0.0.1/32")), Ok(_));
695        assert_matches!(eval.interpret_inline_policy(&ip("ffee::/0")), Ok(_));
696        assert_matches!(eval.interpret_inline_policy(&ip("ffee::/128")), Ok(_));
697
698        // test for parse errors related to prefixes specifically
699        assert_ipaddr_err(eval.interpret_inline_policy(&ip("127.0.0.1/8/24")));
700        assert_ipaddr_err(eval.interpret_inline_policy(&ip("fee::/64::1")));
701        assert_ipaddr_err(eval.interpret_inline_policy(&ip("172.0.0.1/64")));
702        assert_ipaddr_err(eval.interpret_inline_policy(&ip("ffee::/132")));
703        assert_ipaddr_err(eval.interpret_inline_policy(&ip("ffee::/+1")));
704        assert_ipaddr_err(eval.interpret_inline_policy(&ip("ffee::/01")));
705        assert_ipaddr_err(eval.interpret_inline_policy(&ip("ffee::/1234")));
706
707        // test the Display impl
708        assert_eq!(
709            eval.interpret_inline_policy(&ip("127.0.0.1/0"))
710                .unwrap()
711                .to_string(),
712            r#"ip("127.0.0.1/0")"#
713        );
714        assert_eq!(
715            eval.interpret_inline_policy(&ip("127.0.0.1/8"))
716                .unwrap()
717                .to_string(),
718            r#"ip("127.0.0.1/8")"#
719        );
720        assert_eq!(
721            eval.interpret_inline_policy(&ip("127.0.0.1/32"))
722                .unwrap()
723                .to_string(),
724            r#"ip("127.0.0.1/32")"#
725        );
726        assert_eq!(
727            eval.interpret_inline_policy(&ip("ffee::/64"))
728                .unwrap()
729                .to_string(),
730            r#"ip("ffee::/64")"#
731        );
732    }
733
734    #[test]
735    fn ip_equality() {
736        let ext_array = [extension()];
737        let exts = Extensions::specific_extensions(&ext_array).unwrap();
738        let request = basic_request();
739        let entities = basic_entities();
740        let eval = Evaluator::new(request, &entities, &exts);
741
742        // basic equality tests
743        assert_eq!(
744            eval.interpret_inline_policy(&Expr::is_eq(ip("127.0.0.1"), ip("127.0.0.1"))),
745            Ok(Value::from(true))
746        );
747        assert_eq!(
748            eval.interpret_inline_policy(&Expr::is_eq(ip("192.168.0.1"), ip("8.8.8.8"))),
749            Ok(Value::from(false))
750        );
751
752        // weirder equality tests: ipv4 address vs ipv6 address, ip address vs string, ip address vs int
753        assert_eq!(
754            eval.interpret_inline_policy(&Expr::is_eq(ip("127.0.0.1"), ip("::1"))),
755            Ok(Value::from(false))
756        );
757        assert_eq!(
758            eval.interpret_inline_policy(&Expr::is_eq(ip("127.0.0.1"), Expr::val("127.0.0.1"))),
759            Ok(Value::from(false))
760        );
761        assert_eq!(
762            eval.interpret_inline_policy(&Expr::is_eq(ip("::1"), Expr::val(1))),
763            Ok(Value::from(false))
764        );
765
766        // ip address vs range
767        assert_eq!(
768            eval.interpret_inline_policy(&Expr::is_eq(ip("127.0.0.1"), ip("192.168.0.1/24"))),
769            Ok(Value::from(false))
770        );
771        // range vs range
772        assert_eq!(
773            eval.interpret_inline_policy(&Expr::is_eq(ip("192.168.0.1/24"), ip("8.8.8.8/8"))),
774            Ok(Value::from(false))
775        );
776    }
777
778    #[test]
779    fn is_loopback_and_is_multicast() {
780        let ext_array = [extension()];
781        let exts = Extensions::specific_extensions(&ext_array).unwrap();
782        let request = basic_request();
783        let entities = basic_entities();
784        let eval = Evaluator::new(request, &entities, &exts);
785
786        assert_eq!(
787            eval.interpret_inline_policy(&Expr::call_extension_fn(
788                Name::parse_unqualified_name("isLoopback").expect("should be a valid identifier"),
789                vec![ip("127.0.0.2")]
790            )),
791            Ok(Value::from(true))
792        );
793        assert_eq!(
794            eval.interpret_inline_policy(&Expr::call_extension_fn(
795                Name::parse_unqualified_name("isLoopback").expect("should be a valid identifier"),
796                vec![ip("::1")]
797            )),
798            Ok(Value::from(true))
799        );
800        assert_eq!(
801            eval.interpret_inline_policy(&Expr::call_extension_fn(
802                Name::parse_unqualified_name("isLoopback").expect("should be a valid identifier"),
803                vec![ip("::2")]
804            )),
805            Ok(Value::from(false))
806        );
807        assert_eq!(
808            eval.interpret_inline_policy(&Expr::call_extension_fn(
809                Name::parse_unqualified_name("isLoopback").expect("should be a valid identifier"),
810                vec![ip("127.255.200.200/0")]
811            )),
812            Ok(Value::from(false))
813        );
814        assert_eq!(
815            eval.interpret_inline_policy(&Expr::call_extension_fn(
816                Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier"),
817                vec![ip("228.228.228.0")]
818            )),
819            Ok(Value::from(true))
820        );
821        assert_eq!(
822            eval.interpret_inline_policy(&Expr::call_extension_fn(
823                Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier"),
824                vec![ip("224.0.0.0/3")]
825            )),
826            Ok(Value::from(false))
827        );
828        assert_eq!(
829            eval.interpret_inline_policy(&Expr::call_extension_fn(
830                Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier"),
831                vec![ip("224.0.0.0/5")]
832            )),
833            Ok(Value::from(true))
834        );
835        assert_eq!(
836            eval.interpret_inline_policy(&Expr::call_extension_fn(
837                Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier"),
838                vec![ip("ff00::/7")]
839            )),
840            Ok(Value::from(false))
841        );
842        assert_eq!(
843            eval.interpret_inline_policy(&Expr::call_extension_fn(
844                Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier"),
845                vec![ip("ff00::/9")]
846            )),
847            Ok(Value::from(true))
848        );
849        assert_eq!(
850            eval.interpret_inline_policy(&Expr::call_extension_fn(
851                Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier"),
852                vec![ip("127.0.0.1")]
853            )),
854            Ok(Value::from(false))
855        );
856        assert_eq!(
857            eval.interpret_inline_policy(&Expr::call_extension_fn(
858                Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier"),
859                vec![ip("127.0.0.1/1")]
860            )),
861            Ok(Value::from(false))
862        );
863        assert_eq!(
864            eval.interpret_inline_policy(&Expr::call_extension_fn(
865                Name::parse_unqualified_name("isMulticast").expect("should be a valid identifier"),
866                vec![ip("ff00::2")]
867            )),
868            Ok(Value::from(true))
869        );
870    }
871
872    #[test]
873    fn ip_is_in_range() {
874        let ext_array = [extension()];
875        let exts = Extensions::specific_extensions(&ext_array).unwrap();
876        let request = basic_request();
877        let entities = basic_entities();
878        let eval = Evaluator::new(request, &entities, &exts);
879
880        assert_eq!(
881            eval.interpret_inline_policy(&Expr::call_extension_fn(
882                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
883                vec![ip("192.168.0.1/24"), ip("192.168.0.1/24")]
884            )),
885            Ok(Value::from(true))
886        );
887        assert_eq!(
888            eval.interpret_inline_policy(&Expr::call_extension_fn(
889                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
890                vec![ip("192.168.0.1"), ip("192.168.0.1/28")]
891            )),
892            Ok(Value::from(true))
893        );
894        assert_eq!(
895            eval.interpret_inline_policy(&Expr::call_extension_fn(
896                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
897                vec![ip("192.168.0.10"), ip("192.168.0.1/24")]
898            )),
899            Ok(Value::from(true))
900        );
901        assert_eq!(
902            eval.interpret_inline_policy(&Expr::call_extension_fn(
903                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
904                vec![ip("192.168.0.10"), ip("192.168.0.1/28")]
905            )),
906            Ok(Value::from(true))
907        );
908        assert_eq!(
909            eval.interpret_inline_policy(&Expr::call_extension_fn(
910                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
911                vec![ip("192.168.0.75"), ip("192.168.0.1/24")]
912            )),
913            Ok(Value::from(true))
914        );
915        assert_eq!(
916            eval.interpret_inline_policy(&Expr::call_extension_fn(
917                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
918                vec![ip("192.168.0.75"), ip("192.168.0.1/28")]
919            )),
920            Ok(Value::from(false))
921        );
922        // single address is implicitly a /32 range here
923        assert_eq!(
924            eval.interpret_inline_policy(&Expr::call_extension_fn(
925                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
926                vec![ip("192.168.0.1"), ip("192.168.0.1")]
927            )),
928            Ok(Value::from(true))
929        );
930
931        assert_eq!(
932            eval.interpret_inline_policy(&Expr::call_extension_fn(
933                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
934                vec![ip("1:2:3:4::"), ip("1:2:3:4::/48")]
935            )),
936            Ok(Value::from(true))
937        );
938        assert_eq!(
939            eval.interpret_inline_policy(&Expr::call_extension_fn(
940                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
941                vec![ip("1:2:3:4::"), ip("1:2:3:4::/52")]
942            )),
943            Ok(Value::from(true))
944        );
945        assert_eq!(
946            eval.interpret_inline_policy(&Expr::call_extension_fn(
947                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
948                vec![ip("1:2:3:6::"), ip("1:2:3:4::/48")]
949            )),
950            Ok(Value::from(true))
951        );
952        assert_eq!(
953            eval.interpret_inline_policy(&Expr::call_extension_fn(
954                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
955                vec![ip("1:2:3:6::"), ip("1:2:3:4::/52")]
956            )),
957            Ok(Value::from(true))
958        );
959        assert_eq!(
960            eval.interpret_inline_policy(&Expr::call_extension_fn(
961                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
962                vec![ip("1:2:3:ffff::"), ip("1:2:3:4::/48")]
963            )),
964            Ok(Value::from(true))
965        );
966        assert_eq!(
967            eval.interpret_inline_policy(&Expr::call_extension_fn(
968                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
969                vec![ip("1:2:3:ffff::"), ip("1:2:3:4::/52")]
970            )),
971            Ok(Value::from(false))
972        );
973        // single address is implicitly a /128 range here
974        assert_eq!(
975            eval.interpret_inline_policy(&Expr::call_extension_fn(
976                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
977                vec![ip("1:2:3:4::"), ip("1:2:3:4::")]
978            )),
979            Ok(Value::from(true))
980        );
981
982        // test that ipv4 address is not in an ipv6 range
983        assert_eq!(
984            eval.interpret_inline_policy(&Expr::call_extension_fn(
985                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
986                vec![ip("192.168.0.1"), ip("1:2:3:4::/48")]
987            )),
988            Ok(Value::from(false))
989        );
990    }
991
992    #[test]
993    fn more_ip_semantics() {
994        let ext_array = [extension()];
995        let exts = Extensions::specific_extensions(&ext_array).unwrap();
996        let request = basic_request();
997        let entities = basic_entities();
998        let eval = Evaluator::new(request, &entities, &exts);
999
1000        assert_eq!(
1001            eval.interpret_inline_policy(&Expr::is_eq(ip("10.0.0.0"), ip("10.0.0.0"))),
1002            Ok(Value::from(true))
1003        );
1004        assert_eq!(
1005            eval.interpret_inline_policy(&Expr::is_eq(ip("10.0.0.0"), ip("10.0.0.1"))),
1006            Ok(Value::from(false))
1007        );
1008        assert_eq!(
1009            eval.interpret_inline_policy(&Expr::is_eq(ip("10.0.0.0/32"), ip("10.0.0.0"))),
1010            Ok(Value::from(true))
1011        );
1012        assert_eq!(
1013            eval.interpret_inline_policy(&Expr::is_eq(ip("10.0.0.0/24"), ip("10.0.0.0"))),
1014            Ok(Value::from(false))
1015        );
1016        assert_eq!(
1017            eval.interpret_inline_policy(&Expr::is_eq(ip("10.0.0.0/32"), ip("10.0.0.0/32"))),
1018            Ok(Value::from(true))
1019        );
1020        assert_eq!(
1021            eval.interpret_inline_policy(&Expr::is_eq(ip("10.0.0.0/24"), ip("10.0.0.0/32"))),
1022            Ok(Value::from(false))
1023        );
1024        assert_eq!(
1025            eval.interpret_inline_policy(&Expr::is_eq(ip("10.0.0.0/24"), ip("10.0.0.1/24"))),
1026            Ok(Value::from(false))
1027        );
1028        assert_eq!(
1029            eval.interpret_inline_policy(&Expr::is_eq(ip("10.0.0.1/24"), ip("10.0.0.1/29"))),
1030            Ok(Value::from(false))
1031        );
1032        assert_eq!(
1033            eval.interpret_inline_policy(&Expr::call_extension_fn(
1034                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1035                vec![ip("10.0.0.0"), ip("10.0.0.0/24")]
1036            )),
1037            Ok(Value::from(true))
1038        );
1039        assert_eq!(
1040            eval.interpret_inline_policy(&Expr::call_extension_fn(
1041                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1042                vec![ip("10.0.0.0"), ip("10.0.0.0/32")]
1043            )),
1044            Ok(Value::from(true))
1045        );
1046        assert_eq!(
1047            eval.interpret_inline_policy(&Expr::call_extension_fn(
1048                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1049                vec![ip("10.0.0.0"), ip("10.0.0.1/24")]
1050            )),
1051            Ok(Value::from(true))
1052        );
1053        assert_eq!(
1054            eval.interpret_inline_policy(&Expr::call_extension_fn(
1055                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1056                vec![ip("10.0.0.0"), ip("10.0.0.1/32")]
1057            )),
1058            Ok(Value::from(false))
1059        );
1060        assert_eq!(
1061            eval.interpret_inline_policy(&Expr::call_extension_fn(
1062                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1063                vec![ip("10.0.0.1"), ip("10.0.0.0/24")]
1064            )),
1065            Ok(Value::from(true))
1066        );
1067        assert_eq!(
1068            eval.interpret_inline_policy(&Expr::call_extension_fn(
1069                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1070                vec![ip("10.0.0.1"), ip("10.0.0.1/24")]
1071            )),
1072            Ok(Value::from(true))
1073        );
1074        assert_eq!(
1075            eval.interpret_inline_policy(&Expr::call_extension_fn(
1076                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1077                vec![ip("10.0.0.0/24"), ip("10.0.0.0/32")]
1078            )),
1079            Ok(Value::from(false))
1080        );
1081        assert_eq!(
1082            eval.interpret_inline_policy(&Expr::call_extension_fn(
1083                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1084                vec![ip("10.0.0.0/32"), ip("10.0.0.0/24")]
1085            )),
1086            Ok(Value::from(true))
1087        );
1088        assert_eq!(
1089            eval.interpret_inline_policy(&Expr::call_extension_fn(
1090                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1091                vec![ip("10.0.0.1/24"), ip("10.0.0.0/24")]
1092            )),
1093            Ok(Value::from(true))
1094        );
1095        assert_eq!(
1096            eval.interpret_inline_policy(&Expr::call_extension_fn(
1097                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1098                vec![ip("10.0.0.1/24"), ip("10.0.0.1/24")]
1099            )),
1100            Ok(Value::from(true))
1101        );
1102        assert_eq!(
1103            eval.interpret_inline_policy(&Expr::call_extension_fn(
1104                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1105                vec![ip("10.0.0.0/24"), ip("10.0.0.1/24")]
1106            )),
1107            Ok(Value::from(true))
1108        );
1109        assert_eq!(
1110            eval.interpret_inline_policy(&Expr::call_extension_fn(
1111                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1112                vec![ip("10.0.0.0/24"), ip("10.0.0.0/29")]
1113            )),
1114            Ok(Value::from(false))
1115        );
1116        assert_eq!(
1117            eval.interpret_inline_policy(&Expr::call_extension_fn(
1118                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1119                vec![ip("10.0.0.0/29"), ip("10.0.0.0/24")]
1120            )),
1121            Ok(Value::from(true))
1122        );
1123        assert_eq!(
1124            eval.interpret_inline_policy(&Expr::call_extension_fn(
1125                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1126                vec![ip("10.0.0.0/24"), ip("10.0.0.1/29")]
1127            )),
1128            Ok(Value::from(false))
1129        );
1130        assert_eq!(
1131            eval.interpret_inline_policy(&Expr::call_extension_fn(
1132                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1133                vec![ip("10.0.0.0/29"), ip("10.0.0.1/24")]
1134            )),
1135            Ok(Value::from(true))
1136        );
1137        assert_eq!(
1138            eval.interpret_inline_policy(&Expr::call_extension_fn(
1139                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1140                vec![ip("10.0.0.1/24"), ip("10.0.0.0/29")]
1141            )),
1142            Ok(Value::from(false))
1143        );
1144        assert_eq!(
1145            eval.interpret_inline_policy(&Expr::call_extension_fn(
1146                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1147                vec![ip("10.0.0.1/29"), ip("10.0.0.0/24")]
1148            )),
1149            Ok(Value::from(true))
1150        );
1151        assert_eq!(
1152            eval.interpret_inline_policy(&Expr::call_extension_fn(
1153                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1154                vec![ip("10.0.0.0/32"), ip("10.0.0.0/32")]
1155            )),
1156            Ok(Value::from(true))
1157        );
1158        assert_eq!(
1159            eval.interpret_inline_policy(&Expr::call_extension_fn(
1160                Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1161                vec![ip("10.0.0.0/32"), ip("10.0.0.0")]
1162            )),
1163            Ok(Value::from(true))
1164        );
1165        assert_ipaddr_err(eval.interpret_inline_policy(&Expr::call_extension_fn(
1166            Name::parse_unqualified_name("isInRange").expect("should be a valid identifier"),
1167            vec![ip("10.0.0.0/33"), ip("10.0.0.0/32")],
1168        )));
1169    }
1170
1171    #[test]
1172    fn test_contains_at_least_two() {
1173        assert!(contains_at_least_two(":::", ':'));
1174        assert!(contains_at_least_two("::", ':'));
1175        assert!(!contains_at_least_two(":", ':'));
1176    }
1177
1178    #[test]
1179    fn test_contains_two_multibyte() {
1180        assert!(!contains_at_least_two("\u{f1b}", '\u{f1b}'));
1181    }
1182}