pdk-ip-filter-lib 1.7.0

PDK IP Filter Library
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file
use crate::model::network_address::Address;
use crate::model::network_address::Address::{IPv4, IPv6, Unknown};
use crate::model::network_address::{NetworkAddress, ValidIpAddress};

/// Represents a parsed IP address.
pub type AddressType = Address<NetworkAddress>;

/// Parses a string into an [`AddressType`] instance.
pub fn parse_address(address: &str) -> Address<NetworkAddress> {
    parse_address_with(
        address.to_string(),
        NetworkAddress::parse,
        extract_ipv4_embedded_in_ipv6,
    )
}

/// Removes the host from a network address.
pub fn remove_host(network_address: &str) -> String {
    let parts: Vec<&str> = network_address.split('/').collect();
    // If there are 2 parts check if the second part is a mask (numeric)
    if parts.len() == 2 {
        // If the second part is numeric, it is a mask (not a prefix)
        if parts[1].chars().all(|c| c.is_numeric()) {
            return network_address.to_string();
        }
        // If not numeric, the first part is the prefix "host"
        return parts[1].to_string();
    }
    // If there are 3 parts (host/address/mask) remove the prefix "host"
    if parts.len() == 3 {
        return format!("{}/{}", parts[1], parts[2]);
    }
    // Return the original address by default
    network_address.to_string()
}

fn extract_ipv4_embedded_in_ipv6(addr: String) -> String {
    let addr = remove_host(&addr);
    let extract_last_four_octets = |address: &str| {
        address
            .split(':')
            .next_back()
            .unwrap_or_default()
            .to_string()
    };
    if addr.starts_with("::ffff:") {
        extract_last_four_octets(&addr)
    } else {
        addr
    }
}

fn parse_address_with<E, N, F, G>(address: String, parser: F, preprocess: G) -> Address<N>
where
    F: FnOnce(String) -> Result<N, E>,
    G: FnOnce(String) -> String,
    N: ValidIpAddress,
{
    let preprocessed = preprocess(address);
    let net = parser(preprocessed);
    net.map(|address| {
        if address.is_ipv6() {
            IPv6(address)
        } else if address.is_ipv4() {
            IPv4(address)
        } else {
            Unknown
        }
    })
    .unwrap_or(Unknown)
}

#[allow(non_snake_case)]
#[cfg(test)]
mod address_parser_tests {
    extern crate test_case;

    use crate::model::network_address::ValidIpAddress;
    use crate::IpFilterError;

    use super::*;

    mod host_remover_tests {
        use super::remove_host;

        #[test]
        fn given_address_with_mask_and_no_prefix__when_removing_prefix__then_returns_same_address()
        {
            let address = "192.168.0.1/24";

            let address_wo_prefix = remove_host(address);

            assert_eq!(address, address_wo_prefix)
        }

        #[test]
        fn given_address_with_prefix_and_no_mask__when_removing_prefix__then_removes_host() {
            let address = "host/192.168.0.1";

            let address_wo_prefix = remove_host(address);

            assert_eq!("192.168.0.1", address_wo_prefix)
        }

        #[test]
        fn given_address_with_prefix_and_mask__when_removing_prefix__then_removes_host() {
            let address = "host/192.168.0.1/24";

            let address_wo_prefix = remove_host(address);

            assert_eq!("192.168.0.1/24", address_wo_prefix)
        }
    }

    mod ipv4_tests {
        use test_case::test_case;

        use crate::model::network_address::NetworkAddress;

        use super::*;

        #[test_case("192"; "ipv4 missing three octets")]
        #[test_case("..."; "ipv4 all octets empty")]
        #[test_case("0..."; "ipv4 three octets empty")]
        #[test_case("1.2.."; "ipv4 two octets empty")]
        #[test_case("192.0.0.-1"; "ipv4 negative octet")]
        #[test_case("192.0.0.256"; "ipv4 octet greater than 255")]
        fn given_invalid_IPv4_string__when_parsing__then_invalid_ip_is_returned(invalid_ip: &str) {
            assert_eq!(Unknown, parse_address(invalid_ip));
        }

        #[test_case("192.0.0.1", "194.0.0.1"; "ipv4 without port")]
        #[test_case("192.0.0.1:80", "193.0.0.1:80"; "ipv4 socket")]
        #[test_case("host/192.0.0.1", "host/193.0.0.1"; "without port with host/ prefix")]
        #[test_case("host/192.0.0.1:8080", "host/193.0.0.2:8080"; "ipv4 socket with host/ prefix")]
        fn given_valid_ipv4_address__then_address_is_preserved(valid_addr: &str, other_addr: &str) {
            let addr = parse_address(valid_addr);
            let same_addr = parse_address(valid_addr);
            let other_addr = parse_address(other_addr);

            assert_ne!(other_addr, addr);
            assert_eq!(same_addr, addr);
        }

        #[test_case("192.0.0.1"; "ipv4 without port")]
        #[test_case("192.0.0.1:8080"; "ipv4 socket")]
        #[test_case("host/192.0.0.1"; "ipv4 without port with host/ prefix")]
        #[test_case("host/192.0.0.1:8080"; "ipv4 socket host/ prefix")]
        fn given_valid_IPv4_string__when_parsing__then_ipv4_is_returned(addr: &str) {
            let valid_ipv4 = String::from(addr);

            let parsed_address = parse_address(&valid_ipv4);

            assert_is_ipv4(parsed_address)
        }

        fn assert_is_ipv4(parsed_address: Address<NetworkAddress>) {
            match parsed_address {
                IPv4(_) => {}
                _ => panic!(),
            }
        }
    }

    mod ipv6_tests {
        use test_case::test_case;

        use crate::model::network_address::NetworkAddress;

        use super::*;

        #[test_case("2001"; "ipv6 missing three quartets")]
        #[test_case("::::::"; "ipv6 all quartets empty")]
        #[test_case("2001..."; "ipv6 three quartets empty")]
        #[test_case("2001.db8.."; "ipv6 two quartets empty")]
        #[test_case("2001:db8:0:0:0:0:A:-1"; "ipv6 negative quartet")]
        #[test_case("2001:db8:0:0:0:0:A:fffff"; "ipv6 quartet greater than 255")]
        #[test_case("[2001:db8:0:0:0:0:A:0]:"; "ipv6 missing port")]
        fn given_invalid_ipv6_string__when_parsing__then_invalid_ip_is_returned(invalid_ip: &str) {
            assert_eq!(Unknown, parse_address(invalid_ip));
        }

        #[test_case("2001:db8:0:0:0:0:A:0", "2001:db8:0:0:0:0:A:B"; "ipv6 without port")]
        #[test_case("[2001:db8:0:0:0:0:A:0]:8080", "2001:db8:0:0:0:0:A:B"; "ipv6 socket")]
        #[test_case("host/2001:db8:0:0:0:0:A:0", "2001:db8:0:0:0:0:A:B"; "ipv6 without port with host/ prefix")]
        #[test_case("host/[2001:db8:0:0:0:0:A:0]:8080", "2001:db8:0:0:0:0:A:B"; "ipv6 socket host/ prefix")]
        #[test_case("::", "::1"; "abridged ipv6 loopback")]
        #[test_case("::/0", "::1/10"; "abridged ipv6 loopback with mask")]
        fn given_valid_ipv6_address__then_address_is_preserved(valid_addr: &str, other_addr: &str) {
            let addr = parse_address(valid_addr);
            let same_addr = parse_address(valid_addr);
            let other_addr = parse_address(other_addr);

            assert_ne!(other_addr, addr);
            assert_eq!(same_addr, addr);
        }

        #[test_case("2001:db8:0:0:0:0:A:0"; "ipv6 without port")]
        #[test_case("[2001:db8:0:0:0:0:A:0]:8080"; "ipv6 socket")]
        #[test_case("host/2001:db8:0:0:0:0:A:0"; "ipv6 without port with host/ prefix")]
        #[test_case("host/[2001:db8:0:0:0:0:A:0]:8080"; "ipv6 socket host/ prefix")]
        #[test_case("::"; "abridged ipv6 loopback")]
        #[test_case("::/0"; "abridged ipv6 loopback with mask")]
        fn given_valid_IPv6_string__when_parsing__then_ipv6_is_returned(addr: &str) {
            let valid_ipv6 = String::from(addr);

            let parsed_address = parse_address(&valid_ipv6);

            assert_is_ipv6(parsed_address)
        }

        fn assert_is_ipv6(parsed_address: Address<NetworkAddress>) {
            match parsed_address {
                IPv6(_) => {}
                _ => panic!(),
            }
        }

        #[test]
        fn abtirdeg() {
            let abridged = "::/0";
            let addr = parse_address(abridged);

            match addr {
                IPv4(_) => unreachable!(),
                IPv6(_) => {}
                Unknown => unreachable!(),
            }
        }
    }

    struct FakeAddress;

    impl ValidIpAddress for FakeAddress {
        fn is_ipv6(&self) -> bool {
            false
        }
        fn is_ipv4(&self) -> bool {
            false
        }
    }

    #[test]
    fn given_address_str__when_parsed_address_is_not_ipv4_nor_ipv6__then_parse_address_returns_unknown(
    ) {
        let parser = |_s: String| Result::Ok::<FakeAddress, IpFilterError>(FakeAddress);
        let addr = parse_address_with("".parse().unwrap(), parser, |str| str);

        match addr {
            Unknown => {}
            _ => panic!(),
        }
    }

    mod cidr_tests {
        use crate::model::address_parser::parse_address;

        #[test]
        fn given_31_bit_mask__then_contains_two_addresses() {
            let subnet = parse_address(&String::from("192.168.0.0/31"));

            let addr1 = parse_address(&String::from("192.168.0.0"));
            let addr2 = parse_address(&String::from("192.168.0.1"));
            let addr3 = parse_address(&String::from("192.168.0.2"));
            assert!(subnet.contains(&addr1));
            assert!(subnet.contains(&addr2));
            assert!(!subnet.contains(&addr3));
        }

        #[test]
        fn given_ipv4_address_with_octet_missing__when_parsing__then_get_subnet_with_24_bit_mask() {
            let subnet = parse_address(&String::from("192.168.0"));

            let addr1 = parse_address(&String::from("192.168.0.0"));
            let addr2 = parse_address(&String::from("192.168.0.255"));

            assert!(subnet.contains(&addr1));
            assert!(subnet.contains(&addr2));
        }

        #[test]
        fn given_ipv6_address_with_octet_missing__when_parsing__then_get_subnet_with_24_bit_mask() {
            let subnet = parse_address(&String::from("2001:db8:0:0:0:0:A"));

            let addr1 = parse_address(&String::from("2001:db8:0:0:0:0:A:0"));
            let addr2 = parse_address(&String::from("2001:db8:0:0:0:0:A:FFFF"));

            assert!(subnet.contains(&addr1));
            assert!(subnet.contains(&addr2));
        }
    }

    mod ipnet_test {
        use std::net::IpAddr;

        use ipnet::IpNet;

        #[test]
        fn given_valid_ipv4__then_parsing_to_ipnet_success_and_is_ipv4() {
            let parsed_ip = "192.168.0.0".parse::<IpAddr>();
            assert!(parsed_ip.is_ok());
            assert!(parsed_ip.unwrap().is_ipv4());
        }

        #[test]
        fn given_valid_ipv4_cidr_with_32bit_mask__then_contains_that_ip_and_no_other() {
            let net: IpNet = "10.1.1.0/32".parse().unwrap();

            let same_addr1: IpAddr = "10.1.1.0".parse().unwrap();
            let sibling_addr1: IpAddr = "10.1.1.255".parse().unwrap();
            let sibling_addr2: IpAddr = "10.1.0.1".parse().unwrap();
            let sibling_addr3: IpAddr = "10.1.0.255".parse().unwrap();

            assert!(net.contains(&same_addr1));
            assert!(!net.contains(&sibling_addr3));
            assert!(!net.contains(&sibling_addr1));
            assert!(!net.contains(&sibling_addr2));
        }

        #[test]
        fn given_valid_ipv4__then_can_convert_to_ipv4_cidr_with_32bit_mask() {
            let addr: IpAddr = "10.1.1.0".parse().unwrap();
            let net: IpNet = IpNet::from(addr);

            let same_addr1: IpAddr = "10.1.1.0".parse().unwrap();
            let sibling_addr1: IpAddr = "10.1.1.255".parse().unwrap();
            let sibling_addr2: IpAddr = "10.1.0.1".parse().unwrap();
            let sibling_addr3: IpAddr = "10.1.0.255".parse().unwrap();

            assert!(net.contains(&same_addr1));
            assert!(!net.contains(&sibling_addr3));
            assert!(!net.contains(&sibling_addr1));
            assert!(!net.contains(&sibling_addr2));
        }

        #[test]
        fn abridged_loopback_ipv6_decoded_successfully() {
            let abridged: IpAddr = "::1".parse().unwrap();
            let unabridged: IpAddr = "0:0:0:0:0:0:0:1".parse().unwrap();

            assert!(abridged.is_ipv6());
            assert_eq!(unabridged, abridged)
        }

        #[test]
        fn abridged_unspecified_ipv6_decoded_successfully() {
            let abridged: IpAddr = "::".parse().unwrap();
            let unabridged: IpAddr = "0:0:0:0:0:0:0:0".parse().unwrap();

            assert!(abridged.is_ipv6());
            assert_eq!(unabridged, abridged)
        }

        #[test]
        fn abridged_loopback_ipv6_cidr_decoded_successfully() {
            let abridged: IpNet = "::1/0".parse().unwrap();
            let unabridged: IpNet = "0:0:0:0:0:0:0:1/0".parse().unwrap();

            assert!(abridged.addr().is_ipv6());
            assert_eq!(unabridged, abridged)
        }

        #[test]
        fn abridged_unspecified_ipv6_cidr_decoded_successfully() {
            let abridged: IpNet = "::/0".parse().unwrap();
            let unabridged: IpNet = "0:0:0:0:0:0:0:0/0".parse().unwrap();

            assert!(abridged.addr().is_ipv6());
            assert_eq!(unabridged, abridged)
        }
    }

    mod embedded_ipv4_tests {
        use super::*;

        #[test]
        fn parse_ipv4_embedded_in_ipv6_returns_ipv4() {
            let addr = parse_address("::ffff:192.168.1.1");
            assert!(matches!(addr, IPv4(_)));
        }

        #[test]
        fn parse_ipv4_embedded_in_ipv6_with_host_prefix() {
            let addr = parse_address("host/::ffff:10.0.0.1");
            assert!(matches!(addr, IPv4(_)));
        }
    }
}