keyhog_scanner/engine/
phase1_admission.rs1use 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#[derive(Debug)]
36pub struct Phase1AdmissionPlan {
37 admissions: Vec<Phase1Admission>,
38 chunk_shapes: Vec<(usize, usize)>,
39 summary: Phase1AdmissionSummary,
40}
41
42impl Phase1AdmissionPlan {
43 #[must_use]
44 pub fn summary(&self) -> Phase1AdmissionSummary {
45 self.summary
46 }
47
48 #[inline]
49 pub(crate) fn admission_for(&self, index: usize) -> Option<Phase1Admission> {
50 self.admissions.get(index).copied()
51 }
52
53 #[inline]
54 pub(crate) fn matches_chunks(&self, chunks: &[Chunk]) -> bool {
55 chunks.len() == self.chunk_shapes.len()
56 && chunks
57 .iter()
58 .zip(&self.chunk_shapes)
59 .all(|(chunk, &(ptr, len))| {
60 let bytes = chunk.data.as_bytes();
61 bytes.as_ptr() as usize == ptr && bytes.len() == len
62 })
63 }
64}
65
66impl Phase1AdmissionSummary {
67 pub fn all_admitted(chunks: u64, bytes: u64) -> Self {
70 Self {
71 admitted_chunks: chunks,
72 admitted_bytes: bytes,
73 ..Self::default()
74 }
75 }
76
77 #[inline]
78 fn record(&mut self, admission: Phase1Admission, bytes: u64) {
79 match admission {
80 Phase1Admission::AlphabetRejected => {
81 self.alphabet_rejected_chunks += 1;
82 self.alphabet_rejected_bytes += bytes;
83 }
84 Phase1Admission::BigramRejected => {
85 self.bigram_rejected_chunks += 1;
86 self.bigram_rejected_bytes += bytes;
87 }
88 Phase1Admission::Admitted => {
89 self.admitted_chunks += 1;
90 self.admitted_bytes += bytes;
91 }
92 }
93 }
94
95 #[inline]
96 fn merge(self, other: Self) -> Self {
97 Self {
98 alphabet_rejected_chunks: self
99 .alphabet_rejected_chunks
100 .saturating_add(other.alphabet_rejected_chunks),
101 alphabet_rejected_bytes: self
102 .alphabet_rejected_bytes
103 .saturating_add(other.alphabet_rejected_bytes),
104 bigram_rejected_chunks: self
105 .bigram_rejected_chunks
106 .saturating_add(other.bigram_rejected_chunks),
107 bigram_rejected_bytes: self
108 .bigram_rejected_bytes
109 .saturating_add(other.bigram_rejected_bytes),
110 admitted_chunks: self.admitted_chunks.saturating_add(other.admitted_chunks),
111 admitted_bytes: self.admitted_bytes.saturating_add(other.admitted_bytes),
112 }
113 }
114}
115
116impl CompiledScanner {
117 #[inline]
118 pub(crate) fn phase1_admission(&self, data: &[u8]) -> Phase1Admission {
119 if self
120 .alphabet_screen
121 .as_ref()
122 .is_some_and(|screen| !screen.screen(data))
123 {
124 return Phase1Admission::AlphabetRejected;
125 }
126 if data.len() >= BIGRAM_BLOOM_MIN_CHUNK_BYTES && !self.bigram_bloom.maybe_overlaps(data) {
127 return Phase1Admission::BigramRejected;
128 }
129 Phase1Admission::Admitted
130 }
131
132 pub fn phase1_admission_summary(&self, chunks: &[Chunk]) -> Phase1AdmissionSummary {
136 if chunks.len() >= 4
141 && chunks.iter().map(|chunk| chunk.data.len()).sum::<usize>() >= 64 * 1024
142 {
143 use rayon::prelude::*;
144
145 return chunks
146 .par_iter()
147 .map(|chunk| {
148 let mut summary = Phase1AdmissionSummary::default();
149 summary.record(
150 self.phase1_admission(chunk.data.as_bytes()),
151 chunk.data.len() as u64,
152 );
153 summary
154 })
155 .reduce(
156 Phase1AdmissionSummary::default,
157 Phase1AdmissionSummary::merge,
158 );
159 }
160
161 let mut summary = Phase1AdmissionSummary::default();
162 for chunk in chunks {
163 summary.record(
164 self.phase1_admission(chunk.data.as_bytes()),
165 chunk.data.len() as u64,
166 );
167 }
168 summary
169 }
170
171 pub fn phase1_admission_plan(&self, chunks: &[Chunk]) -> Phase1AdmissionPlan {
177 let classified = if chunks.len() >= 4
178 && chunks.iter().map(|chunk| chunk.data.len()).sum::<usize>() >= 64 * 1024
179 {
180 use rayon::prelude::*;
181
182 chunks
183 .par_iter()
184 .map(|chunk| {
185 (
186 self.phase1_admission(chunk.data.as_bytes()),
187 chunk.data.as_bytes().as_ptr() as usize,
188 chunk.data.len(),
189 )
190 })
191 .collect::<Vec<_>>()
192 } else {
193 chunks
194 .iter()
195 .map(|chunk| {
196 (
197 self.phase1_admission(chunk.data.as_bytes()),
198 chunk.data.as_bytes().as_ptr() as usize,
199 chunk.data.len(),
200 )
201 })
202 .collect::<Vec<_>>()
203 };
204 let mut summary = Phase1AdmissionSummary::default();
205 let mut admissions = Vec::with_capacity(classified.len());
206 let mut chunk_shapes = Vec::with_capacity(classified.len());
207 for (admission, ptr, len) in classified {
208 summary.record(admission, len as u64);
209 admissions.push(admission);
210 chunk_shapes.push((ptr, len));
211 }
212 Phase1AdmissionPlan {
213 admissions,
214 chunk_shapes,
215 summary,
216 }
217 }
218}