1use super::{CompiledScanner, BIGRAM_BLOOM_MIN_CHUNK_BYTES};
4use keyhog_core::Chunk;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub(crate) enum Phase1Admission {
8 AlphabetRejected,
9 BigramRejected,
10 Admitted,
11}
12
13#[non_exhaustive]
20#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
21pub struct Phase1AdmissionSummary {
22 pub alphabet_rejected_chunks: u64,
23 pub alphabet_rejected_bytes: u64,
24 pub bigram_rejected_chunks: u64,
25 pub bigram_rejected_bytes: u64,
26 pub admitted_chunks: u64,
27 pub admitted_bytes: u64,
28}
29
30#[non_exhaustive]
36#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
37pub struct Phase2KeywordTriggerSummary {
38 pub keyword_trigger_chunks: u64,
39 pub keyword_trigger_bytes: u64,
40 pub keyword_trigger_count: u64,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub(crate) enum Phase1AdmissionPlanIdentityError {
45 Malformed,
46 Mismatch,
47}
48
49#[derive(Debug)]
55pub struct Phase1AdmissionPlan {
56 admissions: Vec<Phase1Admission>,
57 chunk_shapes: Vec<(usize, usize)>,
58 summary: Phase1AdmissionSummary,
59 phase2_keyword_triggers: Phase2KeywordTriggerSummary,
60}
61
62impl Phase1AdmissionPlan {
63 #[must_use]
64 pub fn summary(&self) -> Phase1AdmissionSummary {
65 self.summary
66 }
67
68 #[must_use]
69 pub fn phase2_keyword_triggers(&self) -> Phase2KeywordTriggerSummary {
70 self.phase2_keyword_triggers
71 }
72
73 #[inline]
74 pub(crate) fn admission_for(&self, index: usize) -> Option<Phase1Admission> {
75 self.admissions.get(index).copied()
76 }
77
78 #[inline]
79 pub(crate) fn validate_chunks(
80 &self,
81 chunks: &[Chunk],
82 ) -> Result<(), Phase1AdmissionPlanIdentityError> {
83 let Some(summary_chunks) = self
84 .summary
85 .alphabet_rejected_chunks
86 .checked_add(self.summary.bigram_rejected_chunks)
87 .and_then(|count| count.checked_add(self.summary.admitted_chunks))
88 else {
89 return Err(Phase1AdmissionPlanIdentityError::Malformed);
90 };
91 let Some(summary_bytes) = self
92 .summary
93 .alphabet_rejected_bytes
94 .checked_add(self.summary.bigram_rejected_bytes)
95 .and_then(|count| count.checked_add(self.summary.admitted_bytes))
96 else {
97 return Err(Phase1AdmissionPlanIdentityError::Malformed);
98 };
99 let Ok(shape_count) = u64::try_from(self.chunk_shapes.len()) else {
100 return Err(Phase1AdmissionPlanIdentityError::Malformed);
101 };
102 let mut shape_bytes = 0u64;
103 for &(_, len) in &self.chunk_shapes {
104 let Ok(len) = u64::try_from(len) else {
105 return Err(Phase1AdmissionPlanIdentityError::Malformed);
106 };
107 let Some(total) = shape_bytes.checked_add(len) else {
108 return Err(Phase1AdmissionPlanIdentityError::Malformed);
109 };
110 shape_bytes = total;
111 }
112 let keyword_summary_valid = self.phase2_keyword_triggers.keyword_trigger_chunks
113 <= shape_count
114 && self.phase2_keyword_triggers.keyword_trigger_bytes <= shape_bytes
115 && self.phase2_keyword_triggers.keyword_trigger_count
116 >= self.phase2_keyword_triggers.keyword_trigger_chunks
117 && (self.phase2_keyword_triggers.keyword_trigger_chunks == 0)
118 == (self.phase2_keyword_triggers.keyword_trigger_count == 0);
119 if self.admissions.len() != self.chunk_shapes.len()
120 || summary_chunks != shape_count
121 || summary_bytes != shape_bytes
122 || !keyword_summary_valid
123 || self
124 .chunk_shapes
125 .iter()
126 .any(|&(ptr, len)| len != 0 && ptr == 0)
127 {
128 return Err(Phase1AdmissionPlanIdentityError::Malformed);
129 }
130 if chunks.len() != self.chunk_shapes.len() {
131 return Err(Phase1AdmissionPlanIdentityError::Malformed);
132 }
133 if !chunks
134 .iter()
135 .zip(&self.chunk_shapes)
136 .all(|(chunk, &(ptr, len))| {
137 let bytes = chunk.data.as_bytes();
138 bytes.as_ptr() as usize == ptr && bytes.len() == len
139 })
140 {
141 return Err(Phase1AdmissionPlanIdentityError::Mismatch);
142 }
143 Ok(())
144 }
145}
146
147impl Phase1AdmissionSummary {
148 pub fn all_admitted(chunks: u64, bytes: u64) -> Self {
151 Self {
152 admitted_chunks: chunks,
153 admitted_bytes: bytes,
154 ..Self::default()
155 }
156 }
157
158 #[inline]
159 fn record(&mut self, admission: Phase1Admission, bytes: u64) {
160 match admission {
161 Phase1Admission::AlphabetRejected => {
162 self.alphabet_rejected_chunks += 1;
163 self.alphabet_rejected_bytes += bytes;
164 }
165 Phase1Admission::BigramRejected => {
166 self.bigram_rejected_chunks += 1;
167 self.bigram_rejected_bytes += bytes;
168 }
169 Phase1Admission::Admitted => {
170 self.admitted_chunks += 1;
171 self.admitted_bytes += bytes;
172 }
173 }
174 }
175
176 #[inline]
177 fn merge(self, other: Self) -> Self {
178 Self {
179 alphabet_rejected_chunks: self
180 .alphabet_rejected_chunks
181 .saturating_add(other.alphabet_rejected_chunks),
182 alphabet_rejected_bytes: self
183 .alphabet_rejected_bytes
184 .saturating_add(other.alphabet_rejected_bytes),
185 bigram_rejected_chunks: self
186 .bigram_rejected_chunks
187 .saturating_add(other.bigram_rejected_chunks),
188 bigram_rejected_bytes: self
189 .bigram_rejected_bytes
190 .saturating_add(other.bigram_rejected_bytes),
191 admitted_chunks: self.admitted_chunks.saturating_add(other.admitted_chunks),
192 admitted_bytes: self.admitted_bytes.saturating_add(other.admitted_bytes),
193 }
194 }
195}
196
197impl CompiledScanner {
198 #[inline]
199 pub(crate) fn phase1_admission(&self, data: &[u8]) -> Phase1Admission {
200 if self
201 .alphabet_screen
202 .as_ref()
203 .is_some_and(|screen| !screen.screen(data))
204 {
205 return Phase1Admission::AlphabetRejected;
206 }
207 if data.len() >= BIGRAM_BLOOM_MIN_CHUNK_BYTES && !self.bigram_bloom.maybe_overlaps(data) {
208 return Phase1Admission::BigramRejected;
209 }
210 Phase1Admission::Admitted
211 }
212
213 #[inline]
214 fn phase1_admission_bypassing_bigram(&self, data: &[u8]) -> Phase1Admission {
215 if self
216 .alphabet_screen
217 .as_ref()
218 .is_some_and(|screen| !screen.screen(data))
219 {
220 return Phase1Admission::AlphabetRejected;
221 }
222 Phase1Admission::Admitted
223 }
224
225 #[inline]
226 fn phase2_keyword_trigger_count(&self, data: &str) -> u64 {
227 self.phase2_keyword_ac.as_ref().map_or(0, |keyword_ac| {
228 keyword_ac
229 .find_iter(data)
230 .fold(0u64, |count, _| count.saturating_add(1))
231 })
232 }
233
234 pub fn phase1_admission_summary(&self, chunks: &[Chunk]) -> Phase1AdmissionSummary {
238 if chunks.len() >= 4
243 && chunks.iter().map(|chunk| chunk.data.len()).sum::<usize>() >= 64 * 1024
244 {
245 use rayon::prelude::*;
246
247 return chunks
248 .par_iter()
249 .map(|chunk| {
250 let mut summary = Phase1AdmissionSummary::default();
251 summary.record(
252 self.phase1_admission(chunk.data.as_bytes()),
253 chunk.data.len() as u64,
254 );
255 summary
256 })
257 .reduce(
258 Phase1AdmissionSummary::default,
259 Phase1AdmissionSummary::merge,
260 );
261 }
262
263 let mut summary = Phase1AdmissionSummary::default();
264 for chunk in chunks {
265 summary.record(
266 self.phase1_admission(chunk.data.as_bytes()),
267 chunk.data.len() as u64,
268 );
269 }
270 summary
271 }
272
273 pub fn phase1_admission_plan(&self, chunks: &[Chunk]) -> Phase1AdmissionPlan {
277 self.phase1_admission_plan_with_bigram_mode(chunks, false)
278 }
279
280 pub fn phase1_admission_plan_bypassing_bigram_for_diagnostics(
287 &self,
288 chunks: &[Chunk],
289 ) -> Phase1AdmissionPlan {
290 self.phase1_admission_plan_with_bigram_mode(chunks, true)
291 }
292
293 fn phase1_admission_plan_with_bigram_mode(
294 &self,
295 chunks: &[Chunk],
296 bypass_bigram: bool,
297 ) -> Phase1AdmissionPlan {
298 let classify = |chunk: &Chunk| {
299 let admission = if bypass_bigram {
300 self.phase1_admission_bypassing_bigram(chunk.data.as_bytes())
301 } else {
302 self.phase1_admission(chunk.data.as_bytes())
303 };
304 (
305 admission,
306 self.phase2_keyword_trigger_count(&chunk.data),
307 chunk.data.as_bytes().as_ptr() as usize,
308 chunk.data.len(),
309 )
310 };
311 let classified = if chunks.len() >= 4
312 && chunks.iter().map(|chunk| chunk.data.len()).sum::<usize>() >= 64 * 1024
313 {
314 use rayon::prelude::*;
315
316 chunks.par_iter().map(classify).collect::<Vec<_>>()
317 } else {
318 chunks.iter().map(classify).collect::<Vec<_>>()
319 };
320 let mut summary = Phase1AdmissionSummary::default();
321 let mut phase2_keyword_triggers = Phase2KeywordTriggerSummary::default();
322 let mut admissions = Vec::with_capacity(classified.len());
323 let mut chunk_shapes = Vec::with_capacity(classified.len());
324 for (admission, keyword_trigger_count, ptr, len) in classified {
325 summary.record(admission, len as u64);
326 if keyword_trigger_count != 0 {
327 phase2_keyword_triggers.keyword_trigger_chunks += 1;
328 phase2_keyword_triggers.keyword_trigger_bytes += len as u64;
329 phase2_keyword_triggers.keyword_trigger_count = phase2_keyword_triggers
330 .keyword_trigger_count
331 .saturating_add(keyword_trigger_count);
332 }
333 admissions.push(admission);
334 chunk_shapes.push((ptr, len));
335 }
336 Phase1AdmissionPlan {
337 admissions,
338 chunk_shapes,
339 summary,
340 phase2_keyword_triggers,
341 }
342 }
343}