Skip to main content

luci/
ip.rs

1//! IP address parsing and normalization for the `ip` field type.
2//!
3//! Supports IPv4 and IPv6. IPv4 addresses are stored as i64 column values
4//! for range queries. Both are stored as normalized keyword strings for
5//! term queries.
6
7use std::net::{IpAddr, Ipv4Addr};
8
9/// Normalize an IP address string for keyword indexing.
10/// Returns the canonical string form, or empty string if invalid.
11pub fn normalize_ip(s: &str) -> String {
12    let s = s.trim();
13    // Handle CIDR notation — strip the prefix length for storage
14    let ip_part = s.split('/').next().unwrap_or(s);
15    match ip_part.parse::<IpAddr>() {
16        Ok(addr) => addr.to_string(),
17        Err(_) => String::new(),
18    }
19}
20
21/// Convert an IP address string to i64 for columnar storage.
22/// IPv4 maps to its u32 numeric value (fits in i64).
23/// IPv6 returns None (range queries not supported for IPv6 yet).
24pub fn ip_to_i64(s: &str) -> Option<i64> {
25    let s = s.trim();
26    let ip_part = s.split('/').next().unwrap_or(s);
27    match ip_part.parse::<IpAddr>() {
28        Ok(IpAddr::V4(v4)) => Some(u32::from(v4) as i64),
29        Ok(IpAddr::V6(v6)) => {
30            // If it's an IPv4-mapped IPv6, extract the IPv4 part
31            if let Some(v4) = v6.to_ipv4_mapped() {
32                Some(u32::from(v4) as i64)
33            } else {
34                None
35            }
36        }
37        Err(_) => None,
38    }
39}
40
41/// Parse a CIDR notation string into (start_ip, end_ip) as i64 values.
42/// Returns None for invalid input or IPv6 CIDRs.
43pub fn cidr_to_range(s: &str) -> Option<(i64, i64)> {
44    let parts: Vec<&str> = s.trim().split('/').collect();
45    if parts.len() != 2 {
46        return None;
47    }
48    let ip: Ipv4Addr = parts[0].parse().ok()?;
49    let prefix_len: u32 = parts[1].parse().ok()?;
50    if prefix_len > 32 {
51        return None;
52    }
53    let ip_num = u32::from(ip);
54    let mask = if prefix_len == 0 {
55        0u32
56    } else {
57        !0u32 << (32 - prefix_len)
58    };
59    let start = ip_num & mask;
60    let end = start | !mask;
61    Some((start as i64, end as i64))
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn normalize_ipv4() {
70        assert_eq!(normalize_ip("192.168.1.1"), "192.168.1.1");
71        assert_eq!(normalize_ip(" 10.0.0.1 "), "10.0.0.1");
72    }
73
74    #[test]
75    fn normalize_ipv6() {
76        assert_eq!(normalize_ip("::1"), "::1");
77        assert_eq!(normalize_ip("2001:db8::1"), "2001:db8::1");
78    }
79
80    #[test]
81    fn normalize_cidr_strips_prefix() {
82        assert_eq!(normalize_ip("192.168.0.0/16"), "192.168.0.0");
83    }
84
85    #[test]
86    fn normalize_invalid() {
87        assert_eq!(normalize_ip("not_an_ip"), "");
88    }
89
90    #[test]
91    fn ipv4_to_i64() {
92        assert_eq!(ip_to_i64("0.0.0.0"), Some(0));
93        assert_eq!(ip_to_i64("0.0.0.1"), Some(1));
94        assert_eq!(ip_to_i64("192.168.1.1"), Some(0xC0A80101));
95        assert_eq!(ip_to_i64("255.255.255.255"), Some(0xFFFFFFFF));
96    }
97
98    #[test]
99    fn ipv6_returns_none() {
100        assert_eq!(ip_to_i64("2001:db8::1"), None);
101    }
102
103    #[test]
104    fn ipv4_mapped_ipv6() {
105        assert_eq!(ip_to_i64("::ffff:192.168.1.1"), Some(0xC0A80101));
106    }
107
108    #[test]
109    fn cidr_range() {
110        let (start, end) = cidr_to_range("192.168.0.0/24").unwrap();
111        assert_eq!(start, ip_to_i64("192.168.0.0").unwrap());
112        assert_eq!(end, ip_to_i64("192.168.0.255").unwrap());
113    }
114
115    #[test]
116    fn cidr_16() {
117        let (start, end) = cidr_to_range("10.0.0.0/16").unwrap();
118        assert_eq!(start, ip_to_i64("10.0.0.0").unwrap());
119        assert_eq!(end, ip_to_i64("10.0.255.255").unwrap());
120    }
121
122    #[test]
123    fn cidr_32() {
124        let (start, end) = cidr_to_range("1.2.3.4/32").unwrap();
125        assert_eq!(start, end);
126        assert_eq!(start, ip_to_i64("1.2.3.4").unwrap());
127    }
128
129    #[test]
130    fn cidr_invalid() {
131        assert!(cidr_to_range("not_cidr").is_none());
132        assert!(cidr_to_range("192.168.0.0").is_none()); // no prefix
133        assert!(cidr_to_range("192.168.0.0/33").is_none()); // prefix too large
134    }
135}