radixtarget_rust/
target.rs

1use crate::dns::{DnsRadixTree, ScopeMode};
2use crate::ip::IpRadixTree;
3use crate::utils::normalize_dns;
4use ipnet::IpNet;
5use std::collections::HashSet;
6use std::net::IpAddr;
7use std::sync::{Arc, Mutex};
8
9#[derive(Clone, Debug)]
10pub struct RadixTarget {
11    dns: DnsRadixTree,
12    ipv4: IpRadixTree,
13    ipv6: IpRadixTree,
14    hosts: HashSet<String>, // store canonicalized hosts for len/contains
15    cached_hash: Arc<Mutex<Option<u64>>>, // cached hash value
16    scope_mode: ScopeMode,  // needed for hash calculation
17}
18
19impl RadixTarget {
20    pub fn new(hosts: &[&str], scope_mode: ScopeMode) -> Self {
21        let dns = DnsRadixTree::new(scope_mode);
22        let acl_mode = scope_mode == ScopeMode::Acl;
23        let mut rt = RadixTarget {
24            dns,
25            ipv4: IpRadixTree::new(acl_mode),
26            ipv6: IpRadixTree::new(acl_mode),
27            hosts: HashSet::new(),
28            cached_hash: Arc::new(Mutex::new(None)),
29            scope_mode,
30        };
31        for &host in hosts {
32            rt.insert(host);
33        }
34        rt
35    }
36
37    /// Insert a target (IP network, IP address, or DNS name). Returns the canonicalized value.
38    pub fn insert(&mut self, value: &str) -> Option<String> {
39        // Invalidate cached hash
40        *self.cached_hash.lock().unwrap() = None;
41
42        // Hosts are now tracked directly in the trees, no need to maintain separate set
43        if let Ok(ipnet) = value.parse::<IpNet>() {
44            match ipnet {
45                IpNet::V4(_) => self.ipv4.insert(ipnet),
46                IpNet::V6(_) => self.ipv6.insert(ipnet),
47            }
48        } else if let Ok(ipaddr) = value.parse::<IpAddr>() {
49            // Convert bare IP address to /32 or /128 network for both storage and return
50            match ipaddr {
51                IpAddr::V4(addr) => {
52                    let net = IpNet::V4(ipnet::Ipv4Net::new(addr, 32).unwrap());
53                    self.ipv4.insert(net)
54                }
55                IpAddr::V6(addr) => {
56                    let net = IpNet::V6(ipnet::Ipv6Net::new(addr, 128).unwrap());
57                    self.ipv6.insert(net)
58                }
59            }
60        } else {
61            let canonical = normalize_dns(value);
62            self.dns.insert(&canonical)
63        }
64    }
65
66    pub fn len(&self) -> usize {
67        self.hosts().len()
68    }
69
70    pub fn strict_scope(&self) -> bool {
71        self.scope_mode == ScopeMode::Strict
72    }
73
74    pub fn is_empty(&self) -> bool {
75        self.hosts().is_empty()
76    }
77
78    pub fn contains(&self, value: &str) -> bool {
79        if let Ok(ipnet) = value.parse::<IpNet>() {
80            match ipnet {
81                IpNet::V4(_) => self.ipv4.get(&ipnet).is_some(),
82                IpNet::V6(_) => self.ipv6.get(&ipnet).is_some(),
83            }
84        } else if let Ok(ipaddr) = value.parse::<IpAddr>() {
85            match ipaddr {
86                IpAddr::V4(addr) => self
87                    .ipv4
88                    .get(&IpNet::V4(ipnet::Ipv4Net::new(addr, 32).unwrap()))
89                    .is_some(),
90                IpAddr::V6(addr) => self
91                    .ipv6
92                    .get(&IpNet::V6(ipnet::Ipv6Net::new(addr, 128).unwrap()))
93                    .is_some(),
94            }
95        } else {
96            let canonical = normalize_dns(value);
97            self.dns.get(&canonical).is_some()
98        }
99    }
100
101    pub fn contains_target(&self, other: &Self) -> bool {
102        other.hosts().iter().all(|host| self.contains(host))
103    }
104
105    /// Delete a target (IP network, IP address, or DNS name). Returns true if deleted.
106    pub fn delete(&mut self, value: &str) -> bool {
107        // Invalidate cached hash
108        *self.cached_hash.lock().unwrap() = None;
109
110        let deleted = if let Ok(ipnet) = value.parse::<IpNet>() {
111            match ipnet {
112                IpNet::V4(_) => self.ipv4.delete(ipnet),
113                IpNet::V6(_) => self.ipv6.delete(ipnet),
114            }
115        } else if let Ok(ipaddr) = value.parse::<IpAddr>() {
116            match ipaddr {
117                IpAddr::V4(addr) => self
118                    .ipv4
119                    .delete(IpNet::V4(ipnet::Ipv4Net::new(addr, 32).unwrap())),
120                IpAddr::V6(addr) => self
121                    .ipv6
122                    .delete(IpNet::V6(ipnet::Ipv6Net::new(addr, 128).unwrap())),
123            }
124        } else {
125            let canonical = normalize_dns(value);
126            self.dns.delete(&canonical)
127        };
128        // Remove the canonical form from hosts, not the original input
129        if deleted && value.parse::<IpNet>().is_err() && value.parse::<IpAddr>().is_err() {
130            let canonical = normalize_dns(value);
131            self.hosts.remove(&canonical);
132        } else {
133            self.hosts.remove(value);
134        }
135        deleted
136    }
137
138    /// Get the most specific match for a target (IP network, IP address, or DNS name). Returns the canonical value if found.
139    pub fn get(&self, value: &str) -> Option<String> {
140        if let Ok(ipnet) = value.parse::<IpNet>() {
141            match ipnet {
142                IpNet::V4(_) => self.ipv4.get(&ipnet),
143                IpNet::V6(_) => self.ipv6.get(&ipnet),
144            }
145        } else if let Ok(ipaddr) = value.parse::<IpAddr>() {
146            match ipaddr {
147                IpAddr::V4(addr) => self
148                    .ipv4
149                    .get(&IpNet::V4(ipnet::Ipv4Net::new(addr, 32).unwrap())),
150                IpAddr::V6(addr) => self
151                    .ipv6
152                    .get(&IpNet::V6(ipnet::Ipv6Net::new(addr, 128).unwrap())),
153            }
154        } else {
155            let canonical = normalize_dns(value);
156            self.dns.get(&canonical)
157        }
158    }
159
160    pub fn prune(&mut self) -> usize {
161        // Invalidate cached hash
162        *self.cached_hash.lock().unwrap() = None;
163        self.dns.prune() + self.ipv4.prune() + self.ipv6.prune()
164    }
165
166    // NOTE: This is a potentially destructive operation
167    // Since in the rust implementation, only the data reference is stored for each node,
168    // defrag will indiscriminately merge nodes regardless of their data
169    // For this reason, this method is not used by the Python implementation, which implements its own defrag logic
170    pub fn defrag(&mut self) -> (HashSet<String>, HashSet<String>) {
171        // Invalidate cached hash
172        *self.cached_hash.lock().unwrap() = None;
173
174        let (cleaned_v4, new_v4) = self.ipv4.defrag();
175        let (cleaned_v6, new_v6) = self.ipv6.defrag();
176        let mut cleaned = HashSet::new();
177        let mut new = HashSet::new();
178        cleaned.extend(cleaned_v4);
179        cleaned.extend(cleaned_v6);
180        new.extend(new_v4);
181        new.extend(new_v6);
182
183        (cleaned, new)
184    }
185
186    pub fn hosts(&self) -> HashSet<String> {
187        let mut all_hosts = HashSet::new();
188
189        // Collect hosts from all trees
190        all_hosts.extend(self.ipv4.hosts());
191        all_hosts.extend(self.ipv6.hosts());
192        all_hosts.extend(self.dns.hosts());
193
194        all_hosts
195    }
196
197    pub fn hash(&self) -> u64 {
198        {
199            let cached = self.cached_hash.lock().unwrap();
200            if let Some(hash_value) = *cached {
201                return hash_value;
202            }
203        }
204
205        let hash_value = self.compute_hash();
206
207        // Cache the result
208        *self.cached_hash.lock().unwrap() = Some(hash_value);
209        hash_value
210    }
211
212    fn compute_hash(&self) -> u64 {
213        // Calculate hash using seahash
214        let mut hosts: Vec<String> = self.hosts().into_iter().collect();
215        hosts.sort();
216
217        // Create a single string to hash
218        let mut data = hosts.join("\n");
219        if self.scope_mode == ScopeMode::Strict {
220            data.push('\0');
221        }
222
223        seahash::hash(data.as_bytes())
224    }
225
226    /// Create a deep copy of this RadixTarget
227    pub fn copy(&self) -> Self {
228        // Clone creates a deep copy of all internal structures
229        let mut cloned = self.clone();
230        // Reset the cached hash since it's wrapped in Arc<Mutex<>>
231        cloned.cached_hash = Arc::new(Mutex::new(None));
232        cloned
233    }
234}
235
236impl PartialEq for RadixTarget {
237    fn eq(&self, other: &Self) -> bool {
238        self.hash() == other.hash()
239    }
240}
241impl Eq for RadixTarget {}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use ipnet::IpNet;
247    use std::collections::HashSet;
248    use std::hash::{Hash, Hasher};
249    use std::str::FromStr;
250
251    fn set_of_strs<I: IntoIterator<Item = String>>(vals: I) -> HashSet<String> {
252        vals.into_iter().collect()
253    }
254
255    #[test]
256    fn test_insert_and_get_ipv4() {
257        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
258        let host = rt.insert("8.8.8.0/24");
259        assert_eq!(host, Some("8.8.8.0/24".to_string()));
260        assert_eq!(rt.get("8.8.8.8/32"), Some("8.8.8.0/24".to_string()));
261        assert_eq!(rt.get("1.1.1.1/32"), None);
262    }
263
264    #[test]
265    fn test_insert_and_get_ipv6() {
266        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
267        let host = rt.insert("dead::/64");
268        assert_eq!(host, Some("dead::/64".to_string()));
269        assert_eq!(rt.get("dead::beef/128"), Some("dead::/64".to_string()));
270        assert_eq!(rt.get("cafe::beef/128"), None);
271    }
272
273    #[test]
274    fn test_insert_and_get_dns() {
275        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
276        let host = rt.insert("example.com");
277        assert_eq!(host, Some("example.com".to_string()));
278        assert_eq!(rt.get("example.com"), Some("example.com".to_string()));
279        assert_eq!(rt.get("notfound.com"), None);
280    }
281
282    #[test]
283    fn test_dns_subdomain_matching() {
284        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
285        let host = rt.insert("api.test.www.example.com");
286        assert_eq!(host, Some("api.test.www.example.com".to_string()));
287        assert_eq!(
288            rt.get("wat.hm.api.test.www.example.com"),
289            Some("api.test.www.example.com".to_string())
290        );
291        assert_eq!(rt.get("notfound.com"), None);
292    }
293
294    #[test]
295    fn test_dns_strict_scope() {
296        let mut rt = RadixTarget::new(&[], ScopeMode::Strict);
297        let host = rt.insert("example.com");
298        assert_eq!(host, Some("example.com".to_string()));
299        assert_eq!(rt.get("example.com"), Some("example.com".to_string()));
300        assert_eq!(rt.get("www.example.com"), None);
301        assert_eq!(rt.get("com"), None);
302    }
303
304    #[test]
305    fn test_delete_ipv4() {
306        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
307        let host = rt.insert("8.8.8.0/24");
308        assert_eq!(host, Some("8.8.8.0/24".to_string()));
309        assert_eq!(rt.get("8.8.8.8/32"), Some("8.8.8.0/24".to_string()));
310        assert!(rt.delete("8.8.8.0/24"));
311        assert_eq!(rt.get("8.8.8.8/32"), None);
312        assert!(!rt.delete("8.8.8.0/24"));
313    }
314
315    #[test]
316    fn test_delete_dns() {
317        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
318        let host = rt.insert("example.com");
319        assert_eq!(host, Some("example.com".to_string()));
320        assert_eq!(rt.get("example.com"), Some("example.com".to_string()));
321        assert!(rt.delete("example.com"));
322        assert_eq!(rt.get("example.com"), None);
323        assert!(!rt.delete("example.com"));
324    }
325
326    #[test]
327    fn test_prune_ip() {
328        // Test IP pruning logic and fallback to less specific parent after manual mutation.
329
330        // 1. Insert two overlapping networks: /24 and /30 (the /30 is a subnet of the /24)
331        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
332        rt.insert("192.168.0.0/24");
333        rt.insert("192.168.0.0/30");
334
335        assert_eq!(rt.get("192.168.0.1"), Some("192.168.0.0/30".to_string()));
336
337        // 2. Walk the tree to the node representing the /30 network.
338        //    This simulates finding the most specific node for 192.168.0.0/30.
339        let mut node = &mut rt.ipv4.root;
340        let slash_thirty = IpNet::from_str("192.168.0.0/30").unwrap();
341        let bits = {
342            let (addr, prefix) = match &slash_thirty {
343                IpNet::V4(n) => (n.network().octets().to_vec(), slash_thirty.prefix_len()),
344                IpNet::V6(n) => (n.network().octets().to_vec(), slash_thirty.prefix_len()),
345            };
346            let mut bits = Vec::with_capacity(prefix as usize);
347            for byte in addr {
348                for i in (0..8).rev() {
349                    if bits.len() == prefix as usize {
350                        break;
351                    }
352                    bits.push((byte >> i) & 1);
353                }
354            }
355            bits
356        };
357        for &bit in &bits[..bits.len() - 1] {
358            node = node.children.get_mut(&(bit as u64)).unwrap();
359        }
360        // At this point, node is the parent of the /30 leaf node.
361        assert_eq!(node.children.len(), 1); // Only the /30 child should exist here.
362        let last_bit = bits[bits.len() - 1] as u64;
363        assert!(node.children.contains_key(&last_bit)); // The /30 node exists.
364
365        // 3. Simulate manual removal of the /30 node's children.
366        //    This mimics a situation where the most specific node is unreachable (e.g., deleted or pruned).
367        node.children.clear();
368
369        // 4. Now, querying for 192.168.0.0 should fall back to the /24 parent network.
370        //    This tests the longest-prefix match/fallback logic.
371        assert_eq!(rt.get("192.168.0.0"), Some("192.168.0.0/24".to_string()));
372
373        // 5. Prune the tree. This should remove all dead nodes left by the manual mutation (5 nodes in this case).
374        let pruned = rt.ipv4.prune();
375        assert_eq!(pruned, 5);
376
377        // 6. Pruning again should do nothing (idempotency check).
378        let pruned2 = rt.ipv4.prune();
379        assert_eq!(pruned2, 0);
380    }
381
382    #[test]
383    fn test_prune_dns() {
384        // dns pruning
385        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
386        rt.insert("example.com");
387        rt.insert("api.test.www.example.com");
388        // Walk to the "api" node
389        let mut node = &mut rt.dns.root;
390        use idna::domain_to_ascii;
391        let segs = ["com", "example", "www", "test"];
392        for seg in segs.iter() {
393            let key = {
394                let canonical = domain_to_ascii(seg).unwrap();
395                let mut hasher = std::collections::hash_map::DefaultHasher::new();
396                canonical.hash(&mut hasher);
397                hasher.finish()
398            };
399            node = node.children.get_mut(&key).unwrap();
400        }
401        assert_eq!(node.children.len(), 1);
402        let api_key = {
403            let canonical = domain_to_ascii("api").unwrap();
404            let mut hasher = std::collections::hash_map::DefaultHasher::new();
405            canonical.hash(&mut hasher);
406            hasher.finish()
407        };
408        assert!(node.children.contains_key(&api_key));
409        // Simulate manual removal of the "api" node's children
410        node.children.clear();
411        // Now the "api" node is unreachable, fallback to "example.com"
412        assert_eq!(
413            rt.get("wat.hm.api.test.www.example.com"),
414            Some("example.com".to_string())
415        );
416        // Prune should remove all dead nodes (2 in this case)
417        let pruned = rt.dns.prune();
418        assert_eq!(pruned, 2);
419        // Pruning again should do nothing
420        let pruned2 = rt.dns.prune();
421        assert_eq!(pruned2, 0);
422    }
423
424    #[test]
425    fn test_defrag_basic_merge() {
426        // Two mergeable subnets
427        let mut target = RadixTarget::new(&[], ScopeMode::Normal);
428        target.insert("192.168.0.0/25");
429        target.insert("192.168.0.128/25");
430        target.insert("www.evilcorp.com");
431        let expected_hosts: HashSet<String> =
432            ["192.168.0.0/25", "192.168.0.128/25", "www.evilcorp.com"]
433                .iter()
434                .map(|s| s.to_string())
435                .collect();
436        assert_eq!(target.hosts(), expected_hosts);
437        let (cleaned, new) = target.defrag();
438        let expected_cleaned: HashSet<String> = ["192.168.0.0/25", "192.168.0.128/25"]
439            .iter()
440            .map(|s| s.to_string())
441            .collect();
442        let expected_new: HashSet<String> =
443            ["192.168.0.0/24".to_string()].iter().cloned().collect();
444        assert_eq!(cleaned, expected_cleaned);
445        assert_eq!(new, expected_new);
446        let expected_hosts_after: HashSet<String> = ["192.168.0.0/24", "www.evilcorp.com"]
447            .iter()
448            .map(|s| s.to_string())
449            .collect();
450        assert_eq!(target.hosts(), expected_hosts_after);
451    }
452
453    #[test]
454    fn test_defrag_recursive_merge_ipv4() {
455        let mut target = RadixTarget::new(&[], ScopeMode::Normal);
456        for net in [
457            "192.168.0.0/25",
458            "192.168.0.128/27",
459            "192.168.0.160/27",
460            "192.168.0.192/27",
461            "192.168.0.224/28",
462            "192.168.0.240/29",
463            "192.168.0.248/30",
464            "192.168.0.252/31",
465            "192.168.0.254/32",
466            "192.168.0.255/32",
467        ]
468        .iter()
469        {
470            target.insert(net);
471        }
472        let expected_hosts: HashSet<String> = [
473            "192.168.0.0/25",
474            "192.168.0.128/27",
475            "192.168.0.160/27",
476            "192.168.0.192/27",
477            "192.168.0.224/28",
478            "192.168.0.240/29",
479            "192.168.0.248/30",
480            "192.168.0.252/31",
481            "192.168.0.254/32", // stored as /32
482            "192.168.0.255/32", // stored as /32
483        ]
484        .iter()
485        .map(|s| s.to_string())
486        .collect();
487        assert_eq!(target.hosts(), expected_hosts);
488        let (cleaned, new) = target.defrag();
489        let expected_cleaned: HashSet<String> = [
490            "192.168.0.0/25",
491            "192.168.0.128/27",
492            "192.168.0.160/27",
493            "192.168.0.192/27",
494            "192.168.0.224/28",
495            "192.168.0.240/29",
496            "192.168.0.248/30",
497            "192.168.0.252/31",
498            "192.168.0.254/32", // defrag returns original tree form
499            "192.168.0.255/32", // defrag returns original tree form
500        ]
501        .iter()
502        .map(|s| s.to_string())
503        .collect();
504        let expected_new: HashSet<String> =
505            ["192.168.0.0/24".to_string()].iter().cloned().collect();
506        assert_eq!(cleaned, expected_cleaned);
507        assert_eq!(new, expected_new);
508        let expected_hosts_after: HashSet<String> =
509            ["192.168.0.0/24".to_string()].iter().cloned().collect();
510        assert_eq!(target.hosts(), expected_hosts_after);
511    }
512
513    #[test]
514    fn test_defrag_recursive_merge_ipv6() {
515        let mut target = RadixTarget::new(&[], ScopeMode::Normal);
516        for net in [
517            "dead:beef::/121",
518            "dead:beef::80/123",
519            "dead:beef::a0/123",
520            "dead:beef::c0/123",
521            "dead:beef::e0/124",
522            "dead:beef::f0/125",
523            "dead:beef::f8/126",
524            "dead:beef::fc/127",
525            "dead:beef::fe/128",
526            "dead:beef::ff/128",
527        ]
528        .iter()
529        {
530            target.insert(net);
531        }
532        let expected_hosts: HashSet<String> = [
533            "dead:beef::/121",
534            "dead:beef::80/123",
535            "dead:beef::a0/123",
536            "dead:beef::c0/123",
537            "dead:beef::e0/124",
538            "dead:beef::f0/125",
539            "dead:beef::f8/126",
540            "dead:beef::fc/127",
541            "dead:beef::fe/128", // stored as /128
542            "dead:beef::ff/128", // stored as /128
543        ]
544        .iter()
545        .map(|s| s.to_string())
546        .collect();
547        assert_eq!(target.hosts(), expected_hosts);
548        let (cleaned, new) = target.defrag();
549        let expected_cleaned: HashSet<String> = [
550            "dead:beef::/121",
551            "dead:beef::80/123",
552            "dead:beef::a0/123",
553            "dead:beef::c0/123",
554            "dead:beef::e0/124",
555            "dead:beef::f0/125",
556            "dead:beef::f8/126",
557            "dead:beef::fc/127",
558            "dead:beef::fe/128", // defrag returns original tree form
559            "dead:beef::ff/128", // defrag returns original tree form
560        ]
561        .iter()
562        .map(|s| s.to_string())
563        .collect();
564        let expected_new: HashSet<String> =
565            ["dead:beef::/120".to_string()].iter().cloned().collect();
566        assert_eq!(cleaned, expected_cleaned);
567        assert_eq!(new, expected_new);
568        let expected_hosts_after: HashSet<String> =
569            ["dead:beef::/120".to_string()].iter().cloned().collect();
570        assert_eq!(target.hosts(), expected_hosts_after);
571    }
572
573    #[test]
574    fn test_defrag_small_recursive() {
575        let mut target = RadixTarget::new(&[], ScopeMode::Normal);
576        // Four /26s covering 192.168.1.0/25 and 192.168.1.128/25
577        target.insert("192.168.1.0/26");
578        target.insert("192.168.1.64/26");
579        target.insert("192.168.1.128/26");
580        target.insert("192.168.1.192/26");
581        target.insert("192.168.0.0/24");
582        // Single defrag: should merge the /26s into /25s, then into a /24, then merge the two /24s into a /23
583        let (cleaned, new) = target.defrag();
584        let expected_cleaned: HashSet<String> = [
585            "192.168.1.0/26",
586            "192.168.1.64/26",
587            "192.168.1.128/26",
588            "192.168.1.192/26",
589            "192.168.0.0/24",
590        ]
591        .iter()
592        .map(|s| s.to_string())
593        .collect();
594        let expected_new: HashSet<String> =
595            ["192.168.0.0/23".to_string()].iter().cloned().collect();
596        assert_eq!(cleaned, expected_cleaned);
597        assert_eq!(new, expected_new);
598    }
599
600    #[test]
601    fn test_insert_malformed_data() {
602        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
603        let malformed_inputs = [
604            "999.999.999.999",        // invalid IPv4
605            "256.256.256.256/33",     // invalid IPv4 CIDR
606            "::gggg",                 // invalid IPv6
607            "dead::beef::cafe",       // invalid IPv6
608            "1.2.3.4/abc",            // invalid CIDR suffix
609            "-example.com",           // invalid DNS (leading hyphen)
610            "example..com",           // double dot
611            ".example.com",           // leading dot
612            "example.com-",           // trailing hyphen
613            "exa mple.com",           // space in domain
614            "",                       // empty string
615            "*.*.*.*",                // wildcard nonsense
616            "[::1]",                  // brackets not allowed
617            "1.2.3.4/",               // trailing slash
618            "com..",                  // trailing double dot
619            "...",                    // just dots
620            "foo@bar.com",            // @ in domain
621            "1.2.3.4.5",              // too many octets
622            "1234:5678:9abc:defg::1", // invalid hex in IPv6
623            "example_com",            // underscore in domain
624        ];
625        for input in malformed_inputs.iter() {
626            // Should not panic, should insert as DNS fallback, or handle gracefully
627            let _ = rt.insert(input);
628            // Should not be retrievable as a valid IP or network
629            assert_eq!(
630                rt.get(input),
631                rt.dns.get(input),
632                "Malformed input should only be in DNS tree: {}",
633                input
634            );
635        }
636    }
637
638    #[test]
639    fn test_hash_same_hosts_different_order() {
640        // Test that targets with same hosts in different order have same hash
641        let mut rt1 = RadixTarget::new(&[], ScopeMode::Normal);
642        let mut rt2 = RadixTarget::new(&[], ScopeMode::Normal);
643
644        // Add hosts in different orders
645        rt1.insert("example.com");
646        rt1.insert("192.168.1.0/24");
647        rt1.insert("test.org");
648        rt1.insert("10.0.0.0/8");
649
650        rt2.insert("10.0.0.0/8");
651        rt2.insert("test.org");
652        rt2.insert("192.168.1.0/24");
653        rt2.insert("example.com");
654
655        let hash1 = rt1.hash();
656        let hash2 = rt2.hash();
657
658        assert_eq!(
659            hash1, hash2,
660            "Targets with same hosts in different order should have same hash"
661        );
662        assert_eq!(rt1, rt2, "Targets with same hosts should be equal");
663    }
664
665    #[test]
666    fn test_hash_strict_vs_non_strict() {
667        // Test that strict and non-strict targets with same hosts have different hashes
668        let mut rt_strict = RadixTarget::new(&[], ScopeMode::Strict);
669        let mut rt_non_strict = RadixTarget::new(&[], ScopeMode::Normal);
670
671        // Add same hosts to both
672        rt_strict.insert("example.com");
673        rt_strict.insert("192.168.1.0/24");
674
675        rt_non_strict.insert("example.com");
676        rt_non_strict.insert("192.168.1.0/24");
677
678        let hash_strict = rt_strict.hash();
679        let hash_non_strict = rt_non_strict.hash();
680
681        assert_ne!(
682            hash_strict, hash_non_strict,
683            "Strict and non-strict targets should have different hashes"
684        );
685        assert_ne!(
686            rt_strict, rt_non_strict,
687            "Targets should not be equal since equality is now hash-based"
688        );
689    }
690
691    #[test]
692    fn test_hash_missing_host_scenario() {
693        // Test hash equality before and after adding missing host
694        let mut rt1 = RadixTarget::new(&[], ScopeMode::Normal);
695        let mut rt2 = RadixTarget::new(&[], ScopeMode::Normal);
696
697        // rt1 has all hosts, rt2 is missing one
698        rt1.insert("example.com");
699        rt1.insert("192.168.1.0/24");
700        rt1.insert("test.org");
701
702        rt2.insert("example.com");
703        rt2.insert("192.168.1.0/24");
704
705        let hash1_before = rt1.hash();
706        let hash2_before = rt2.hash();
707
708        assert_ne!(
709            hash1_before, hash2_before,
710            "Targets with different hosts should have different hashes"
711        );
712        assert_ne!(rt1, rt2, "Targets with different hosts should not be equal");
713
714        // Add missing host to rt2
715        rt2.insert("test.org");
716
717        let hash1_after = rt1.hash();
718        let hash2_after = rt2.hash();
719
720        assert_eq!(
721            hash1_after, hash2_after,
722            "Targets should have same hash after adding missing host"
723        );
724        assert_eq!(
725            rt1, rt2,
726            "Targets should be equal after adding missing host"
727        );
728
729        // Verify that rt1's hash didn't change (it was cached)
730        assert_eq!(
731            hash1_before, hash1_after,
732            "rt1 hash should remain the same (cached)"
733        );
734    }
735
736    #[test]
737    fn test_hash_caching_and_invalidation() {
738        // Test that hash is cached and invalidated properly
739        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
740
741        rt.insert("example.com");
742        rt.insert("192.168.1.0/24");
743
744        // Get hash twice - should be same (cached)
745        let hash1 = rt.hash();
746        let hash2 = rt.hash();
747        assert_eq!(
748            hash1, hash2,
749            "Consecutive hash calls should return same value"
750        );
751
752        // Insert new host - should invalidate cache
753        rt.insert("test.org");
754        let hash3 = rt.hash();
755        assert_ne!(hash1, hash3, "Hash should change after inserting new host");
756
757        // Delete host - should invalidate cache
758        let hash4 = rt.hash(); // Cache the current hash
759        rt.delete("test.org");
760        let hash5 = rt.hash();
761        assert_ne!(hash4, hash5, "Hash should change after deleting host");
762        assert_eq!(
763            hash1, hash5,
764            "Hash should return to original after deleting added host"
765        );
766
767        // Prune - should invalidate cache
768        let _hash6 = rt.hash();
769        rt.prune();
770        let _hash7 = rt.hash();
771        // Hash might be same if no pruning occurred, but cache should still be invalidated
772        // We can't easily test this without the actual cache state, but the method should work
773
774        // Defrag - should invalidate cache
775        let _hash8 = rt.hash();
776        rt.defrag();
777        let _hash9 = rt.hash();
778        // Similar to prune, hash might be same but cache should be invalidated
779    }
780
781    #[test]
782    fn test_empty_target_hash() {
783        // Test hash of empty targets
784        let rt1 = RadixTarget::new(&[], ScopeMode::Normal);
785        let rt2 = RadixTarget::new(&[], ScopeMode::Normal);
786        let rt3 = RadixTarget::new(&[], ScopeMode::Strict);
787
788        let hash1 = rt1.hash();
789        let hash2 = rt2.hash();
790        let hash3 = rt3.hash();
791
792        assert_eq!(
793            hash1, hash2,
794            "Empty non-strict targets should have same hash"
795        );
796        assert_ne!(
797            hash1, hash3,
798            "Empty strict and non-strict targets should have different hashes"
799        );
800    }
801
802    #[test]
803    fn test_hash_consistency_across_operations() {
804        // Test that hash remains consistent across various operations
805        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
806
807        // Build up target
808        rt.insert("example.com");
809        rt.insert("192.168.1.0/24");
810        rt.insert("test.org");
811        let final_hash = rt.hash();
812
813        // Create another target with same hosts and same strict_scope
814        let mut rt2 = RadixTarget::new(&[], ScopeMode::Normal);
815        rt2.insert("test.org");
816        rt2.insert("example.com");
817        rt2.insert("192.168.1.0/24");
818
819        assert_eq!(
820            final_hash,
821            rt2.hash(),
822            "Final hashes should be equal regardless of insertion order"
823        );
824        assert_eq!(
825            rt, rt2,
826            "Targets should be equal with same hosts and same strict_scope"
827        );
828
829        // Test that adding and removing the same host doesn't change hash
830        let original_hash = rt.hash();
831        rt.insert("temp.com");
832        rt.delete("temp.com");
833        let restored_hash = rt.hash();
834
835        assert_eq!(
836            original_hash, restored_hash,
837            "Hash should be same after adding and removing same host"
838        );
839    }
840
841    #[test]
842    fn test_equality_with_same_strict_scope() {
843        // Test that targets with same hosts and same strict_scope are equal
844        let mut rt1_strict = RadixTarget::new(&[], ScopeMode::Strict);
845        let mut rt2_strict = RadixTarget::new(&[], ScopeMode::Strict);
846        let mut rt1_non_strict = RadixTarget::new(&[], ScopeMode::Normal);
847        let mut rt2_non_strict = RadixTarget::new(&[], ScopeMode::Normal);
848
849        // Add same hosts to all targets
850        for rt in [
851            &mut rt1_strict,
852            &mut rt2_strict,
853            &mut rt1_non_strict,
854            &mut rt2_non_strict,
855        ] {
856            rt.insert("example.com");
857            rt.insert("192.168.1.0/24");
858        }
859
860        // Targets with same strict_scope should be equal
861        assert_eq!(
862            rt1_strict, rt2_strict,
863            "Strict targets with same hosts should be equal"
864        );
865        assert_eq!(
866            rt1_non_strict, rt2_non_strict,
867            "Non-strict targets with same hosts should be equal"
868        );
869
870        // Targets with different strict_scope should not be equal
871        assert_ne!(
872            rt1_strict, rt1_non_strict,
873            "Strict and non-strict targets should not be equal"
874        );
875        assert_ne!(
876            rt2_strict, rt2_non_strict,
877            "Strict and non-strict targets should not be equal"
878        );
879    }
880
881    #[test]
882    fn test_ip_normalization_single_hosts() {
883        // Test that single host IPs are consistently stored as /32 or /128 networks
884        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
885
886        // Insert individual IPv4 address
887        rt.insert("192.168.1.100");
888        // Should be stored as /32 network
889        assert!(
890            rt.hosts().contains("192.168.1.100/32"),
891            "IPv4 address should be stored as /32"
892        );
893        assert!(
894            !rt.hosts().contains("192.168.1.100"),
895            "IPv4 address should not be stored without /32 suffix"
896        );
897
898        // Insert individual IPv6 address
899        rt.insert("dead::beef");
900        // Should be stored as /128 network
901        assert!(
902            rt.hosts().contains("dead::beef/128"),
903            "IPv6 address should be stored as /128"
904        );
905        assert!(
906            !rt.hosts().contains("dead::beef"),
907            "IPv6 address should not be stored without /128 suffix"
908        );
909
910        // Insert /32 IPv4 network explicitly
911        rt.insert("10.0.0.1/32");
912        assert!(
913            rt.hosts().contains("10.0.0.1/32"),
914            "IPv4 /32 should be stored as /32"
915        );
916        assert!(
917            !rt.hosts().contains("10.0.0.1"),
918            "IPv4 /32 should not be stored without /32 suffix"
919        );
920
921        // Insert /128 IPv6 network explicitly
922        rt.insert("cafe::1/128");
923        assert!(
924            rt.hosts().contains("cafe::1/128"),
925            "IPv6 /128 should be stored as /128"
926        );
927        assert!(
928            !rt.hosts().contains("cafe::1"),
929            "IPv6 /128 should not be stored without /128 suffix"
930        );
931
932        // Insert IP networks with actual network bits (should remain as-is)
933        rt.insert("10.0.0.0/8");
934        rt.insert("cafe::/64");
935        assert!(
936            rt.hosts().contains("10.0.0.0/8"),
937            "IPv4 network should remain as-is"
938        );
939        assert!(
940            rt.hosts().contains("cafe::/64"),
941            "IPv6 network should remain as-is"
942        );
943
944        // Insert DNS name (should remain as-is)
945        rt.insert("example.com");
946        assert!(
947            rt.hosts().contains("example.com"),
948            "DNS name should remain as-is"
949        );
950
951        // Verify that searching works correctly with normalization
952        assert!(
953            rt.hosts().contains("192.168.1.100/32"),
954            "Should find IPv4 address"
955        );
956        assert!(
957            rt.hosts().contains("dead::beef/128"),
958            "Should find IPv6 address"
959        );
960        assert_eq!(
961            rt.get("192.168.1.100"),
962            rt.get("192.168.1.100/32"),
963            "IPv4 lookups should be equivalent"
964        );
965        assert_eq!(
966            rt.get("dead::beef"),
967            rt.get("dead::beef/128"),
968            "IPv6 lookups should be equivalent"
969        );
970
971        // Check final hosts set contains normalized forms
972        let expected_hosts: HashSet<String> = [
973            "192.168.1.100/32", // stored as /32
974            "dead::beef/128",   // stored as /128
975            "10.0.0.1/32",      // stored as /32
976            "cafe::1/128",      // stored as /128
977            "10.0.0.0/8",       // network remains as-is
978            "cafe::/64",        // network remains as-is
979            "example.com",      // DNS remains as-is
980        ]
981        .iter()
982        .map(|s| s.to_string())
983        .collect();
984        assert_eq!(
985            rt.hosts(),
986            expected_hosts,
987            "Hosts should contain consistent network forms"
988        );
989    }
990
991    #[test]
992    fn test_dns_case_normalization() {
993        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
994
995        // Insert with mixed case
996        let host1 = rt.insert("Example.COM");
997
998        // All case variations should find the same entry
999        assert_eq!(rt.get("example.com"), host1.clone());
1000        assert_eq!(rt.get("EXAMPLE.COM"), host1.clone());
1001        assert_eq!(rt.get("Example.Com"), host1.clone());
1002        assert_eq!(rt.get("eXaMpLe.CoM"), host1);
1003
1004        // Contains should work with all case variations
1005        assert!(rt.contains("example.com"));
1006        assert!(rt.contains("EXAMPLE.COM"));
1007        assert!(rt.contains("Example.Com"));
1008        assert!(rt.contains("eXaMpLe.CoM"));
1009
1010        // Delete should work with any case variation
1011        assert!(rt.delete("EXAMPLE.com"));
1012        assert_eq!(rt.get("example.com"), None);
1013        assert!(!rt.contains("Example.COM"));
1014    }
1015
1016    #[test]
1017    fn test_dns_idna_normalization() {
1018        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
1019
1020        // Unicode domain that gets converted to punycode
1021        let unicode = "café.com";
1022        let punycode = "xn--caf-dma.com";
1023
1024        // Insert unicode
1025        let host1 = rt.insert(unicode);
1026
1027        assert_eq!(rt.hosts(), set_of_strs(vec!["xn--caf-dma.com".to_string()]));
1028
1029        // Should be able to find with both unicode and punycode
1030        assert_eq!(rt.get(unicode), host1.clone());
1031        assert_eq!(rt.get(punycode), host1.clone());
1032        assert_eq!(rt.get("CAFÉ.COM"), host1.clone());
1033        assert_eq!(rt.get("XN--CAF-DMA.COM"), host1);
1034
1035        // Contains should work with both forms
1036        assert!(rt.contains(unicode));
1037        assert!(rt.contains(punycode));
1038        assert!(rt.contains("CAFÉ.COM"));
1039        assert!(rt.contains("XN--CAF-DMA.COM"));
1040
1041        // Delete with punycode should work
1042        assert!(rt.delete(punycode));
1043        assert_eq!(rt.get(unicode), None);
1044        assert!(!rt.contains("CAFÉ.COM"));
1045    }
1046
1047    #[test]
1048    fn test_dns_mixed_case_and_idna() {
1049        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
1050
1051        // Insert with mixed case unicode
1052        let host1 = rt.insert("CAFÉ.COM");
1053
1054        // All variations should work
1055        assert_eq!(rt.get("café.com"), host1.clone());
1056        assert_eq!(rt.get("CAFÉ.COM"), host1.clone());
1057        assert_eq!(rt.get("Café.Com"), host1.clone());
1058        assert_eq!(rt.get("xn--caf-dma.com"), host1.clone());
1059        assert_eq!(rt.get("XN--CAF-DMA.COM"), host1);
1060
1061        // Delete with lowercase unicode
1062        assert!(rt.delete("café.com"));
1063        assert_eq!(rt.get("CAFÉ.COM"), None);
1064    }
1065
1066    #[test]
1067    fn test_contains_target() {
1068        // Test basic containment scenarios
1069        let mut superset = RadixTarget::new(&[], ScopeMode::Normal);
1070        let mut subset = RadixTarget::new(&[], ScopeMode::Normal);
1071        let mut disjoint = RadixTarget::new(&[], ScopeMode::Normal);
1072
1073        // Setup superset with broad coverage
1074        superset.insert("example.com");
1075        superset.insert("192.168.0.0/16");
1076        superset.insert("test.org");
1077        superset.insert("10.0.0.0/8");
1078        superset.insert("dead:beef::/32");
1079
1080        // Setup subset with targets covered by superset
1081        subset.insert("sub.example.com"); // covered by example.com
1082        subset.insert("192.168.1.100"); // covered by 192.168.0.0/16
1083        subset.insert("10.5.5.5"); // covered by 10.0.0.0/8
1084
1085        // Setup disjoint set with only some overlap
1086        disjoint.insert("example.com");
1087        disjoint.insert("172.16.1.0/24");
1088
1089        // Test containment relationships
1090        assert!(
1091            superset.contains_target(&subset),
1092            "Superset should contain subset"
1093        );
1094        assert!(
1095            !subset.contains_target(&superset),
1096            "Subset should not contain superset"
1097        );
1098        assert!(
1099            !superset.contains_target(&disjoint),
1100            "Superset should not contain disjoint set"
1101        );
1102        assert!(
1103            !disjoint.contains_target(&superset),
1104            "Disjoint set should not contain superset"
1105        );
1106        assert!(
1107            !subset.contains_target(&disjoint),
1108            "Subset should not contain disjoint set"
1109        );
1110
1111        // Test self-containment
1112        assert!(
1113            superset.contains_target(&superset),
1114            "Target should contain itself"
1115        );
1116        assert!(
1117            subset.contains_target(&subset),
1118            "Target should contain itself"
1119        );
1120        assert!(
1121            disjoint.contains_target(&disjoint),
1122            "Target should contain itself"
1123        );
1124
1125        // Test empty target containment
1126        let empty = RadixTarget::new(&[], ScopeMode::Normal);
1127        assert!(
1128            superset.contains_target(&empty),
1129            "Any target should contain empty target"
1130        );
1131        assert!(
1132            subset.contains_target(&empty),
1133            "Any target should contain empty target"
1134        );
1135        assert!(
1136            empty.contains_target(&empty),
1137            "Empty target should contain itself"
1138        );
1139        assert!(
1140            !empty.contains_target(&superset),
1141            "Empty target should not contain non-empty target"
1142        );
1143    }
1144
1145    #[test]
1146    fn test_contains_target_ip_networks() {
1147        let mut broad = RadixTarget::new(&[], ScopeMode::Normal);
1148        let mut specific = RadixTarget::new(&[], ScopeMode::Normal);
1149
1150        // Broad network coverage
1151        broad.insert("192.168.0.0/16");
1152        broad.insert("10.0.0.0/8");
1153        broad.insert("2001:db8::/32");
1154
1155        // Specific networks within broad coverage
1156        specific.insert("192.168.1.0/24"); // subset of 192.168.0.0/16
1157        specific.insert("10.5.0.0/16"); // subset of 10.0.0.0/8
1158        specific.insert("2001:db8:1::/48"); // subset of 2001:db8::/32
1159
1160        assert!(
1161            broad.contains_target(&specific),
1162            "Broad networks should contain specific subnets"
1163        );
1164        assert!(
1165            !specific.contains_target(&broad),
1166            "Specific networks should not contain broader networks"
1167        );
1168
1169        // Test exact matches
1170        let mut exact = RadixTarget::new(&[], ScopeMode::Normal);
1171        exact.insert("192.168.0.0/16");
1172        assert!(
1173            broad.contains_target(&exact),
1174            "Should contain exact network match"
1175        );
1176        assert!(exact.contains_target(&exact), "Should contain itself");
1177
1178        // Test individual IPs
1179        let mut single_ips = RadixTarget::new(&[], ScopeMode::Normal);
1180        single_ips.insert("192.168.1.100"); // covered by 192.168.0.0/16
1181        single_ips.insert("10.0.0.1"); // covered by 10.0.0.0/8
1182        single_ips.insert("2001:db8::1"); // covered by 2001:db8::/32
1183
1184        assert!(
1185            broad.contains_target(&single_ips),
1186            "Broad networks should contain individual IPs within range"
1187        );
1188    }
1189
1190    #[test]
1191    fn test_contains_target_dns_hierarchies() {
1192        let mut parent = RadixTarget::new(&[], ScopeMode::Normal);
1193        let mut child = RadixTarget::new(&[], ScopeMode::Normal);
1194
1195        // Parent domain coverage (Normal mode allows subdomain matching)
1196        parent.insert("example.com");
1197        parent.insert("test.org");
1198
1199        // Child domains
1200        child.insert("api.example.com");
1201        child.insert("www.example.com");
1202        child.insert("sub.test.org");
1203
1204        assert!(
1205            parent.contains_target(&child),
1206            "Parent domains should contain subdomains in Normal mode"
1207        );
1208
1209        // Test with exact domain matches
1210        let mut exact = RadixTarget::new(&[], ScopeMode::Normal);
1211        exact.insert("example.com");
1212        assert!(
1213            parent.contains_target(&exact),
1214            "Should contain exact domain match"
1215        );
1216    }
1217
1218    #[test]
1219    fn test_contains_target_strict_scope() {
1220        let mut strict_parent = RadixTarget::new(&[], ScopeMode::Strict);
1221        let mut strict_child = RadixTarget::new(&[], ScopeMode::Strict);
1222
1223        // In strict mode, subdomains are not automatically matched
1224        strict_parent.insert("example.com");
1225        strict_child.insert("www.example.com");
1226
1227        assert!(
1228            !strict_parent.contains_target(&strict_child),
1229            "Parent domain should not contain subdomain in Strict mode"
1230        );
1231
1232        // But exact matches should work
1233        let mut exact = RadixTarget::new(&[], ScopeMode::Strict);
1234        exact.insert("example.com");
1235        assert!(
1236            strict_parent.contains_target(&exact),
1237            "Should contain exact match in Strict mode"
1238        );
1239    }
1240
1241    #[test]
1242    fn test_contains_target_mixed_types() {
1243        let mut mixed_superset = RadixTarget::new(&[], ScopeMode::Normal);
1244        let mut mixed_subset = RadixTarget::new(&[], ScopeMode::Normal);
1245
1246        // Superset with various types
1247        mixed_superset.insert("example.com"); // DNS
1248        mixed_superset.insert("192.168.0.0/16"); // IPv4 network
1249        mixed_superset.insert("10.0.0.1"); // IPv4 address
1250        mixed_superset.insert("2001:db8::/32"); // IPv6 network
1251
1252        // Subset with targets covered by superset
1253        mixed_subset.insert("api.example.com"); // covered by example.com
1254        mixed_subset.insert("192.168.1.100"); // covered by 192.168.0.0/16
1255        mixed_subset.insert("10.0.0.1"); // exact match
1256        mixed_subset.insert("2001:db8:1::1"); // covered by 2001:db8::/32
1257
1258        assert!(
1259            mixed_superset.contains_target(&mixed_subset),
1260            "Mixed superset should contain mixed subset"
1261        );
1262
1263        // Add something not covered
1264        mixed_subset.insert("unrelated.net");
1265        assert!(
1266            !mixed_superset.contains_target(&mixed_subset),
1267            "Should not contain subset with uncovered elements"
1268        );
1269    }
1270
1271    #[test]
1272    fn test_contains_target_partial_overlap() {
1273        let mut target1 = RadixTarget::new(&[], ScopeMode::Normal);
1274        let mut target2 = RadixTarget::new(&[], ScopeMode::Normal);
1275
1276        // Partially overlapping sets
1277        target1.insert("example.com");
1278        target1.insert("192.168.0.0/24");
1279        target1.insert("shared.net");
1280
1281        target2.insert("test.org");
1282        target2.insert("10.0.0.0/8");
1283        target2.insert("shared.net");
1284
1285        // Neither should contain the other
1286        assert!(
1287            !target1.contains_target(&target2),
1288            "Partially overlapping sets should not contain each other"
1289        );
1290        assert!(
1291            !target2.contains_target(&target1),
1292            "Partially overlapping sets should not contain each other"
1293        );
1294
1295        // Test with just the shared element
1296        let mut shared_only = RadixTarget::new(&[], ScopeMode::Normal);
1297        shared_only.insert("shared.net");
1298        assert!(
1299            target1.contains_target(&shared_only),
1300            "Should contain subset with only shared elements"
1301        );
1302        assert!(
1303            target2.contains_target(&shared_only),
1304            "Should contain subset with only shared elements"
1305        );
1306    }
1307}
1308
1309#[cfg(test)]
1310mod benchmarks {
1311    use super::*;
1312    use std::fs;
1313    use std::net::Ipv4Addr;
1314    use std::time::Instant;
1315
1316    fn load_cidrs() -> Vec<String> {
1317        let cidr_path = "radixtarget/test/cidrs.txt";
1318        fs::read_to_string(cidr_path)
1319            .unwrap_or_else(|_| panic!("Failed to read {}", cidr_path))
1320            .lines()
1321            .filter(|line| !line.trim().is_empty())
1322            .map(|line| line.trim().to_string())
1323            .collect()
1324    }
1325
1326    #[test]
1327    #[ignore] // Use `cargo test --ignored` to run benchmarks
1328    fn bench_insertion_performance() {
1329        let cidrs = load_cidrs();
1330        println!(
1331            "📊 Loading {} CIDR blocks for insertion benchmark",
1332            cidrs.len()
1333        );
1334
1335        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
1336
1337        println!("🚀 Starting insertion benchmark...");
1338        let start = Instant::now();
1339
1340        for cidr in &cidrs {
1341            rt.insert(cidr);
1342        }
1343
1344        let elapsed = start.elapsed();
1345        let insertions_per_second = (cidrs.len() as f64 / elapsed.as_secs_f64()) as u64;
1346
1347        println!("📈 Insertion Benchmark Results:");
1348        println!(
1349            "  {} insertions in {:.4} seconds",
1350            cidrs.len(),
1351            elapsed.as_secs_f64()
1352        );
1353        println!("  {} insertions/second", insertions_per_second);
1354        println!("  Target contains {} hosts", rt.len());
1355
1356        // Verify some insertions worked
1357        assert!(rt.contains("100.20.0.0/14"));
1358        assert!(rt.get("100.20.1.1").is_some());
1359
1360        println!(
1361            "✓ Insertion benchmark completed: {} insertions/second",
1362            insertions_per_second
1363        );
1364    }
1365
1366    #[test]
1367    #[ignore] // Use `cargo test --ignored` to run benchmarks
1368    fn bench_lookup_performance() {
1369        let cidrs = load_cidrs();
1370        println!(
1371            "📊 Loading {} CIDR blocks for lookup benchmark",
1372            cidrs.len()
1373        );
1374
1375        let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
1376
1377        // Insert all CIDRs first
1378        for cidr in &cidrs {
1379            rt.insert(cidr);
1380        }
1381
1382        println!("✅ Loaded {} CIDR blocks", cidrs.len());
1383
1384        // Generate random IPv4 addresses for lookup testing
1385        let iterations = 100_000;
1386        println!("📋 Pre-generating {} test IPs...", iterations);
1387
1388        let mut test_ips = Vec::with_capacity(iterations);
1389        use std::collections::hash_map::DefaultHasher;
1390        use std::hash::{Hash, Hasher};
1391
1392        for i in 0..iterations {
1393            // Use a simple PRNG based on index for reproducible results
1394            let mut hasher = DefaultHasher::new();
1395            i.hash(&mut hasher);
1396            let random_u32 = (hasher.finish() % (u32::MAX as u64)) as u32;
1397            let ip = Ipv4Addr::from(random_u32);
1398            test_ips.push(ip.to_string());
1399        }
1400
1401        println!("🚀 Running lookup benchmark...");
1402        let start = Instant::now();
1403        let mut hits = 0;
1404        let mut misses = 0;
1405
1406        for ip in &test_ips {
1407            match rt.get(ip) {
1408                Some(_) => hits += 1,
1409                None => misses += 1,
1410            }
1411        }
1412
1413        let elapsed = start.elapsed();
1414        let lookups_per_second = (iterations as f64 / elapsed.as_secs_f64()) as u64;
1415
1416        println!("📈 Lookup Benchmark Results:");
1417        println!(
1418            "  {} iterations in {:.4} seconds",
1419            iterations,
1420            elapsed.as_secs_f64()
1421        );
1422        println!("  {} lookups/second", lookups_per_second);
1423        println!("  {} hits, {} misses", hits, misses);
1424        println!(
1425            "  Hit rate: {:.1}%",
1426            (hits as f64 / iterations as f64) * 100.0
1427        );
1428
1429        println!(
1430            "✓ Lookup benchmark completed: {} lookups/second",
1431            lookups_per_second
1432        );
1433    }
1434}