1use kernel::{AxError, Locator};
31
32use crate::offload::{OffloadSite, tee};
33
34pub mod diff;
35pub mod filter;
36pub mod key;
37pub mod record;
38pub(crate) mod scan;
39pub(crate) mod stages;
40
41pub use diff::SieveHistory;
42pub use filter::{Filter, FilterTable};
43pub use key::CommandKey;
44pub use record::{
45 PassReason, ResultOffloaded, SieveAccount, SieveRecord, Sieved, Stage, StageOutcome,
46 StageReport,
47};
48
49use scan::{Priority, is_protected, priority};
50use stages::Cuts;
51
52pub const SIEVE_FLOOR: usize = 2048;
54pub const MAX_TOTAL_LINES: usize = 240;
56pub const HEAD_LINES: usize = 40;
57pub const TAIL_LINES: usize = 40;
58pub const MIDDLE_LINES: usize = 60;
59
60pub struct SieveInput<'a> {
61 pub key: &'a CommandKey,
62 pub exit_code: Option<i64>,
63 pub text: &'a str,
64}
65
66fn bytes_of(lines: &[String]) -> u64 {
67 lines
68 .iter()
69 .map(|l| u64::try_from(l.len()).unwrap_or(u64::MAX).saturating_add(1))
70 .fold(0u64, u64::saturating_add)
71}
72
73struct Draft {
76 lines: Vec<String>,
77 account: Vec<StageReport>,
78}
79
80impl Draft {
81 fn step(&mut self, stage: Stage, pass: impl FnOnce(&[String]) -> Vec<String>) {
82 let candidate = pass(&self.lines);
83 self.accept(stage, candidate);
84 }
85
86 fn accept(&mut self, stage: Stage, candidate: Vec<String>) {
89 let outcome = if candidate == self.lines {
90 StageOutcome::Noop
91 } else {
92 let before = bytes_of(&self.lines);
93 let after = bytes_of(&candidate);
94 if after > before {
95 StageOutcome::Rejected { grew_to: after }
96 } else {
97 self.lines = candidate;
98 StageOutcome::Applied {
99 bytes_before: before,
100 bytes_after: after,
101 }
102 }
103 };
104 self.account.push(StageReport { stage, outcome });
105 }
106
107 fn unavailable(&mut self, stage: Stage, reason: &str) {
108 self.account.push(StageReport {
109 stage,
110 outcome: StageOutcome::Unavailable {
111 reason: reason.to_owned(),
112 },
113 });
114 }
115}
116
117fn filter_marks(lines: &[String], filter: &Filter) -> Vec<Option<Priority>> {
122 let mut marks: Vec<Option<Priority>> = vec![None; lines.len()];
123 let mut carried: Option<Priority> = None;
124 for (index, line) in lines.iter().enumerate() {
125 let mark = if filter.keeps(line) {
126 let head = priority(line)
127 .unwrap_or(Priority::Warning)
128 .min(Priority::Warning);
129 carried = filter.group_until_blank.then_some(head);
130 Some(head)
131 } else if line.trim().is_empty() {
132 carried = None;
133 None
134 } else {
135 carried
136 };
137 if let Some(slot) = marks.get_mut(index) {
138 *slot = mark;
139 }
140 }
141 marks
142}
143
144fn normalised(lines: &[String], filter: &Filter) -> Vec<String> {
147 let stripped = if filter.strip_ansi {
148 stages::strip_ansi(lines)
149 } else {
150 lines.to_vec()
151 };
152 let folded = stages::fold_blank(&stripped);
153 let exempt: Vec<bool> = filter_marks(&folded, filter)
154 .iter()
155 .map(Option::is_some)
156 .collect();
157 stages::dedup_templates(&folded, &exempt)
158}
159
160fn previous_lines(
163 previous: Option<Locator>,
164 site: &mut OffloadSite<'_>,
165 filter: &Filter,
166) -> Result<Vec<String>, &'static str> {
167 let Some(Locator::Cas { hash, .. }) = previous else {
168 return Err("no previous run of this command in this run");
169 };
170 let bytes = site
171 .cas
172 .get(&hash)
173 .map_err(|_| "previous original is no longer in the CAS")?;
174 let text = String::from_utf8_lossy(&bytes);
175 let lines: Vec<String> = text.lines().map(str::to_owned).collect();
176 Ok(normalised(&lines, filter))
177}
178
179fn marks(lines: &[String], filter: &Filter) -> Vec<Option<Priority>> {
182 lines
183 .iter()
184 .zip(filter_marks(lines, filter))
185 .map(|(line, kept)| match (priority(line), kept) {
186 (Some(own), Some(k)) => Some(own.min(k)),
187 (own, kept) => own.or(kept),
188 })
189 .collect()
190}
191
192fn size(bytes: u64) -> String {
193 if bytes < 1024 {
194 return format!("{bytes} B");
195 }
196 let tenths = bytes.saturating_mul(10).checked_div(1024).unwrap_or(0);
197 format!(
198 "{}.{} KiB",
199 tenths.checked_div(10).unwrap_or(0),
200 tenths.checked_rem(10).unwrap_or(0)
201 )
202}
203
204fn grouped(n: u64) -> String {
205 let digits = n.to_string();
206 let mut out = String::new();
207 for (i, c) in digits.chars().enumerate() {
208 if i > 0 && (digits.len().saturating_sub(i)).checked_rem(3) == Some(0) {
209 out.push(',');
210 }
211 out.push(c);
212 }
213 out
214}
215
216pub fn sieve(
221 input: SieveInput<'_>,
222 table: &FilterTable,
223 site: &mut OffloadSite<'_>,
224 history: &mut SieveHistory,
225) -> Result<Sieved, AxError> {
226 let text = input.text;
227 if text.len() <= SIEVE_FLOOR {
228 return Ok(Sieved::Passed {
229 text: text.to_owned(),
230 reason: PassReason::BelowFloor,
231 account: None,
234 });
235 }
236 let pinned = tee(text.as_bytes(), site)?;
237 let previous = history.previous(input.key).cloned();
238 history.record(input.key.clone(), pinned.original.clone());
239 let filter = table.lookup(input.key);
240 let lines: Vec<String> = text.lines().map(str::to_owned).collect();
241 let lines_in = u64::try_from(lines.len()).unwrap_or(u64::MAX);
242 let mut draft = Draft {
243 lines,
244 account: Vec::with_capacity(7),
245 };
246
247 if filter.strip_ansi {
248 draft.step(Stage::StripAnsi, stages::strip_ansi);
249 } else {
250 draft.unavailable(Stage::StripAnsi, "disabled by the filter");
251 }
252 draft.step(Stage::FoldBlank, stages::fold_blank);
253 draft.step(Stage::DedupTemplate, |now| {
254 let exempt: Vec<bool> = filter_marks(now, filter)
255 .iter()
256 .map(Option::is_some)
257 .collect();
258 stages::dedup_templates(now, &exempt)
259 });
260 match previous_lines(previous, site, filter) {
261 Ok(before) => draft.step(Stage::DiffPrevious, |now| diff::only_changes(now, &before)),
262 Err(reason) => draft.unavailable(Stage::DiffPrevious, reason),
263 }
264 draft.step(Stage::Filter, |now| {
265 let kept: Vec<String> = now
266 .iter()
267 .filter(|l| filter.keeps(l) || is_protected(l) || !filter.drops(l))
268 .cloned()
269 .collect();
270 if kept.iter().all(|l| l.trim().is_empty()) {
271 vec![filter.when_empty(input.key, input.exit_code)]
272 } else {
273 kept
274 }
275 });
276 draft.step(Stage::CutLongLine, stages::cut_long_lines);
277 let cuts = Cuts {
278 max_lines: MAX_TOTAL_LINES,
279 head: filter
280 .head
281 .and_then(|h| usize::try_from(h).ok())
282 .unwrap_or(HEAD_LINES),
283 tail: filter
284 .tail
285 .and_then(|t| usize::try_from(t).ok())
286 .unwrap_or(TAIL_LINES),
287 middle: MIDDLE_LINES,
288 };
289 draft.step(Stage::Truncate, |now| {
290 stages::truncate(now, &marks(now, filter), cuts)
291 });
292
293 let Draft { lines, account } = draft;
294 let body = lines.join("\n");
295 let bytes_in = u64::try_from(text.len()).unwrap_or(u64::MAX);
296 let body_len = u64::try_from(body.len()).unwrap_or(u64::MAX);
297 let lines_out = u64::try_from(lines.len()).unwrap_or(u64::MAX);
298 let footer = format!(
299 "[sieve: {} → {} lines, {} → {}, filter={}, rest at {}]",
300 grouped(lines_in),
301 grouped(lines_out),
302 size(bytes_in),
303 size(body_len),
304 filter.id,
305 pinned.rest_path
306 );
307 let out = format!("{body}\n{footer}");
308 if out.len() > text.len() {
309 return Ok(Sieved::Passed {
314 text: text.to_owned(),
315 reason: PassReason::NothingShrank,
316 account: Some(SieveAccount {
317 filter: filter.id.clone(),
318 lines_in,
319 lines_out: lines_in,
322 stages: account,
323 }),
324 });
325 }
326 let bytes_out = u64::try_from(out.len()).unwrap_or(u64::MAX);
327 Ok(Sieved::Cut(SieveRecord {
328 text: out,
329 original: pinned.original,
330 rest_path: pinned.rest_path,
331 filter: filter.id.clone(),
332 lines_in,
333 lines_out,
334 bytes_in,
335 bytes_out,
336 stages: account,
337 }))
338}
339
340#[cfg(test)]
341#[allow(
342 clippy::unwrap_used,
343 clippy::expect_used,
344 clippy::panic,
345 clippy::indexing_slicing,
346 clippy::arithmetic_side_effects,
347 clippy::as_conversions,
348 reason = "test code"
349)]
350mod tests;