1use std::collections::HashMap;
9
10use crate::{GlobalDictionary, invalid};
11use rudb_common::Result;
12
13pub(crate) const CAPACITY: usize = 512;
14pub(crate) const BYTE_BUDGET: usize = 1024 * 1024;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct HostSummary {
19 pub column: usize,
21 pub omitted_max: u64,
23 pub entries: Vec<HostEntry>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct HostEntry {
30 pub host: String,
31 pub count: u64,
32 pub bytes_sum: i128,
33 pub minimum: String,
34}
35
36pub(crate) fn host_bytes(text: &[u8]) -> &[u8] {
38 let rest = text.strip_prefix(b"http://").or_else(|| text.strip_prefix(b"https://"));
39 let Some(rest) = rest else { return text };
40 let Some(end) = rest.iter().position(|&byte| byte == b'/') else { return text };
41 if end == 0 || rest[end + 1..].contains(&b'\n') {
42 return text;
43 }
44 let host = &rest[..end];
45 host.strip_prefix(b"www.").filter(|without| !without.is_empty()).unwrap_or(host)
46}
47
48pub(crate) fn build(
49 column: usize,
50 dictionary: &GlobalDictionary,
51 flat: &[u8],
52 bases: &[u64],
53) -> Result<Option<HostSummary>> {
54 let mut candidates = HashMap::<Vec<u8>, u64>::new();
55 let mut omitted_max = 0_u64;
56 for (code, &weight) in dictionary.counts.iter().enumerate() {
57 if weight == 0 {
58 continue;
59 }
60 let (from, to) = GlobalDictionary::value_span(&dictionary.ends, bases, code);
61 let text = flat
62 .get(from..to)
63 .ok_or_else(|| invalid("host source code is outside its dictionary"))?;
64 if text.is_empty() {
65 continue;
66 }
67 let host = host_bytes(text);
68 let mut remaining = weight;
69 loop {
70 if let Some(count) = candidates.get_mut(host) {
71 *count =
72 count.checked_add(remaining).ok_or_else(|| invalid("host count overflow"))?;
73 break;
74 }
75 if candidates.len() < CAPACITY {
76 candidates.insert(host.to_vec(), remaining);
77 break;
78 }
79 let least = candidates.values().copied().min().unwrap_or_default();
80 let subtract = remaining.min(least);
81 candidates.retain(|_, count| {
82 *count -= subtract;
83 *count != 0
84 });
85 omitted_max =
86 omitted_max.checked_add(subtract).ok_or_else(|| invalid("host bound overflow"))?;
87 remaining -= subtract;
88 if remaining == 0 {
89 break;
90 }
91 }
92 }
93
94 let mut exact = candidates
95 .into_keys()
96 .map(|host| (host, (0_u64, 0_i128, Vec::<u8>::new())))
97 .collect::<HashMap<_, _>>();
98 for (code, &weight) in dictionary.counts.iter().enumerate() {
99 if weight == 0 {
100 continue;
101 }
102 let (from, to) = GlobalDictionary::value_span(&dictionary.ends, bases, code);
103 let text = flat
104 .get(from..to)
105 .ok_or_else(|| invalid("host source code is outside its dictionary"))?;
106 if text.is_empty() {
107 continue;
108 }
109 let Some((count, bytes_sum, minimum)) = exact.get_mut(host_bytes(text)) else { continue };
110 *count = count.checked_add(weight).ok_or_else(|| invalid("host count overflow"))?;
111 let bytes =
112 i128::try_from(text.len()).map_err(|_| invalid("host source length overflow"))?;
113 *bytes_sum = bytes_sum
114 .checked_add(
115 bytes
116 .checked_mul(i128::from(weight))
117 .ok_or_else(|| invalid("host length sum overflow"))?,
118 )
119 .ok_or_else(|| invalid("host length sum overflow"))?;
120 if minimum.is_empty() || text < minimum.as_slice() {
121 *minimum = text.to_vec();
122 }
123 }
124 let mut entries = exact
125 .into_iter()
126 .filter(|(_, (count, _, _))| *count != 0)
127 .map(|(host, (count, bytes_sum, minimum))| {
128 Ok(HostEntry {
129 host: String::from_utf8(host).map_err(|_| invalid("host is not UTF-8"))?,
130 count,
131 bytes_sum,
132 minimum: String::from_utf8(minimum)
133 .map_err(|_| invalid("host minimum is not UTF-8"))?,
134 })
135 })
136 .collect::<Result<Vec<_>>>()?;
137 entries.sort_unstable_by(|left, right| {
138 right.count.cmp(&left.count).then_with(|| left.host.cmp(&right.host))
139 });
140 let bytes = entries.iter().try_fold(0_usize, |sum, entry| {
141 sum.checked_add(entry.host.len())?.checked_add(entry.minimum.len())
142 });
143 if bytes.is_none_or(|bytes| bytes > BYTE_BUDGET) {
144 return Ok(None);
145 }
146 Ok(Some(HostSummary { column, omitted_max, entries }))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::host_bytes;
152
153 #[test]
154 fn host_expression_keeps_anchored_regex_boundaries() {
155 assert_eq!(host_bytes(b"http://www.example.com/a"), b"example.com");
156 assert_eq!(host_bytes(b"http://example.com"), b"http://example.com");
157 assert_eq!(host_bytes(b"https:///a"), b"https:///a");
158 assert_eq!(host_bytes(b"https://example.com/a\nb"), b"https://example.com/a\nb");
159 assert_eq!(host_bytes(b"http://www./a"), b"www.");
160 }
161}