browser_forensic_core/
analyze.rs1use crate::BrowserEvent;
4
5pub fn rare_domains(events: &[BrowserEvent], cap: usize) -> Vec<(String, usize)> {
10 let mut domain_counts: std::collections::HashMap<String, usize> =
11 std::collections::HashMap::new();
12
13 for event in events {
14 if let Some(url_val) = event.attrs.get("url") {
15 if let Some(url_str) = url_val.as_str() {
16 if let Ok(parsed) = url::Url::parse(url_str) {
17 if let Some(host) = parsed.host_str() {
18 *domain_counts.entry(host.to_string()).or_insert(0) += 1;
19 }
20 }
21 }
22 }
23 }
24
25 let mut result: Vec<(String, usize)> = domain_counts
26 .into_iter()
27 .filter(|(_, count)| *count <= cap)
28 .collect();
29
30 result.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
31 result
32}
33
34#[cfg(test)]
35mod tests {
36 use super::*;
37 use crate::{ArtifactKind, BrowserEvent, BrowserFamily};
38 use serde_json::json;
39
40 fn make_history_event(url: &str) -> BrowserEvent {
41 BrowserEvent::new(
42 0,
43 BrowserFamily::Chromium,
44 ArtifactKind::History,
45 "source",
46 url,
47 )
48 .with_attr("url", json!(url))
49 }
50
51 #[test]
52 fn rare_domains_empty_events_returns_empty() {
53 let result = rare_domains(&[], 5);
54 assert!(result.is_empty());
55 }
56
57 #[test]
58 fn rare_domains_below_cap_returned() {
59 let events = vec![make_history_event("https://rare.example.com/page")];
60 let result = rare_domains(&events, 5);
61 assert!(result.iter().any(|(d, _)| d == "rare.example.com"));
62 }
63
64 #[test]
65 fn rare_domains_above_cap_excluded() {
66 let events: Vec<BrowserEvent> = (0..10)
68 .map(|i| make_history_event(&format!("https://popular.com/page{i}")))
69 .collect();
70 let result = rare_domains(&events, 5);
71 assert!(!result.iter().any(|(d, _)| d == "popular.com"));
72 }
73
74 #[test]
75 fn rare_domains_sorted_by_count_ascending() {
76 let mut events = vec![make_history_event("https://rare.com/page")];
78 events.push(make_history_event("https://semi-rare.com/a"));
80 events.push(make_history_event("https://semi-rare.com/b"));
81
82 let result = rare_domains(&events, 5);
83 let rare_pos = result.iter().position(|(d, _)| d == "rare.com");
84 let semi_pos = result.iter().position(|(d, _)| d == "semi-rare.com");
85 assert!(rare_pos.is_some() && semi_pos.is_some());
86 assert!(rare_pos.unwrap() < semi_pos.unwrap());
87 }
88}