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>, cached_hash: Arc<Mutex<Option<u64>>>, scope_mode: ScopeMode, }
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 pub fn insert(&mut self, value: &str) -> Option<String> {
39 *self.cached_hash.lock().unwrap() = None;
41
42 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 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 pub fn delete(&mut self, value: &str) -> bool {
107 *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 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 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 *self.cached_hash.lock().unwrap() = None;
163 self.dns.prune() + self.ipv4.prune() + self.ipv6.prune()
164 }
165
166 pub fn defrag(&mut self) -> (HashSet<String>, HashSet<String>) {
171 *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 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 *self.cached_hash.lock().unwrap() = Some(hash_value);
209 hash_value
210 }
211
212 fn compute_hash(&self) -> u64 {
213 let mut hosts: Vec<String> = self.hosts().into_iter().collect();
215 hosts.sort();
216
217 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 pub fn copy(&self) -> Self {
228 let mut cloned = self.clone();
230 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 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 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 assert_eq!(node.children.len(), 1); let last_bit = bits[bits.len() - 1] as u64;
363 assert!(node.children.contains_key(&last_bit)); node.children.clear();
368
369 assert_eq!(rt.get("192.168.0.0"), Some("192.168.0.0/24".to_string()));
372
373 let pruned = rt.ipv4.prune();
375 assert_eq!(pruned, 5);
376
377 let pruned2 = rt.ipv4.prune();
379 assert_eq!(pruned2, 0);
380 }
381
382 #[test]
383 fn test_prune_dns() {
384 let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
386 rt.insert("example.com");
387 rt.insert("api.test.www.example.com");
388 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 node.children.clear();
411 assert_eq!(
413 rt.get("wat.hm.api.test.www.example.com"),
414 Some("example.com".to_string())
415 );
416 let pruned = rt.dns.prune();
418 assert_eq!(pruned, 2);
419 let pruned2 = rt.dns.prune();
421 assert_eq!(pruned2, 0);
422 }
423
424 #[test]
425 fn test_defrag_basic_merge() {
426 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", "192.168.0.255/32", ]
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", "192.168.0.255/32", ]
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", "dead:beef::ff/128", ]
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", "dead:beef::ff/128", ]
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 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 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", "256.256.256.256/33", "::gggg", "dead::beef::cafe", "1.2.3.4/abc", "-example.com", "example..com", ".example.com", "example.com-", "exa mple.com", "", "*.*.*.*", "[::1]", "1.2.3.4/", "com..", "...", "foo@bar.com", "1.2.3.4.5", "1234:5678:9abc:defg::1", "example_com", ];
625 for input in malformed_inputs.iter() {
626 let _ = rt.insert(input);
628 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 let mut rt1 = RadixTarget::new(&[], ScopeMode::Normal);
642 let mut rt2 = RadixTarget::new(&[], ScopeMode::Normal);
643
644 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 let mut rt_strict = RadixTarget::new(&[], ScopeMode::Strict);
669 let mut rt_non_strict = RadixTarget::new(&[], ScopeMode::Normal);
670
671 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 let mut rt1 = RadixTarget::new(&[], ScopeMode::Normal);
695 let mut rt2 = RadixTarget::new(&[], ScopeMode::Normal);
696
697 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 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 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 let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
740
741 rt.insert("example.com");
742 rt.insert("192.168.1.0/24");
743
744 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 rt.insert("test.org");
754 let hash3 = rt.hash();
755 assert_ne!(hash1, hash3, "Hash should change after inserting new host");
756
757 let hash4 = rt.hash(); 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 let _hash6 = rt.hash();
769 rt.prune();
770 let _hash7 = rt.hash();
771 let _hash8 = rt.hash();
776 rt.defrag();
777 let _hash9 = rt.hash();
778 }
780
781 #[test]
782 fn test_empty_target_hash() {
783 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 let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
806
807 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 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 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 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 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 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 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 let mut rt = RadixTarget::new(&[], ScopeMode::Normal);
885
886 rt.insert("192.168.1.100");
888 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 rt.insert("dead::beef");
900 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 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 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 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 rt.insert("example.com");
946 assert!(
947 rt.hosts().contains("example.com"),
948 "DNS name should remain as-is"
949 );
950
951 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 let expected_hosts: HashSet<String> = [
973 "192.168.1.100/32", "dead::beef/128", "10.0.0.1/32", "cafe::1/128", "10.0.0.0/8", "cafe::/64", "example.com", ]
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 let host1 = rt.insert("Example.COM");
997
998 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 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 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 let unicode = "café.com";
1022 let punycode = "xn--caf-dma.com";
1023
1024 let host1 = rt.insert(unicode);
1026
1027 assert_eq!(rt.hosts(), set_of_strs(vec!["xn--caf-dma.com".to_string()]));
1028
1029 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 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 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 let host1 = rt.insert("CAFÉ.COM");
1053
1054 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 assert!(rt.delete("café.com"));
1063 assert_eq!(rt.get("CAFÉ.COM"), None);
1064 }
1065
1066 #[test]
1067 fn test_contains_target() {
1068 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 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 subset.insert("sub.example.com"); subset.insert("192.168.1.100"); subset.insert("10.5.5.5"); disjoint.insert("example.com");
1087 disjoint.insert("172.16.1.0/24");
1088
1089 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 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 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.insert("192.168.0.0/16");
1152 broad.insert("10.0.0.0/8");
1153 broad.insert("2001:db8::/32");
1154
1155 specific.insert("192.168.1.0/24"); specific.insert("10.5.0.0/16"); specific.insert("2001:db8:1::/48"); 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 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 let mut single_ips = RadixTarget::new(&[], ScopeMode::Normal);
1180 single_ips.insert("192.168.1.100"); single_ips.insert("10.0.0.1"); single_ips.insert("2001:db8::1"); 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.insert("example.com");
1197 parent.insert("test.org");
1198
1199 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 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 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 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 mixed_superset.insert("example.com"); mixed_superset.insert("192.168.0.0/16"); mixed_superset.insert("10.0.0.1"); mixed_superset.insert("2001:db8::/32"); mixed_subset.insert("api.example.com"); mixed_subset.insert("192.168.1.100"); mixed_subset.insert("10.0.0.1"); mixed_subset.insert("2001:db8:1::1"); assert!(
1259 mixed_superset.contains_target(&mixed_subset),
1260 "Mixed superset should contain mixed subset"
1261 );
1262
1263 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 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 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 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] 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 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] 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 for cidr in &cidrs {
1379 rt.insert(cidr);
1380 }
1381
1382 println!("✅ Loaded {} CIDR blocks", cidrs.len());
1383
1384 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 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}