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