git_policy_cli/candidate/scanner_run.rs
1//! What:
2//! Scan every eligible candidate of one version through the loaded scanner.
3//! Why:
4//! This is the whole forbidden-strings check of one policy pass:
5//! candidate bytes
6//! come from the candidate store (staged or committed,
7//! never the worktree),
8//! go to
9//! the scanner in memory,
10//! and the scanner's own results come back.
11//! No temporary
12//! file is written and no process is started per file.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! // const scans = await scanVersion(scanner, store, version, rulesPath);
17//! ```
18
19/// Import the candidate layer's failure,
20/// store and version.
21use crate::candidate_error::CandidateError;
22use crate::candidate_store::CandidateStore;
23use crate::candidate_version::{Candidate, CandidateVersion};
24/// Import the candidates whose content a parent already holds.
25use crate::candidate_known_content::KnownContent;
26/// Import the loaded scanner and its failure.
27use crate::scanner_adapter::{CandidateScanner, ScannerError};
28/// Import the eligibility decision.
29use crate::scanner_selection::is_scannable;
30/// The scanner library's per-candidate result,
31/// returned unchanged.
32use forbidden_strings::CandidateScan;
33/// `Rc<T>` is a shared,
34/// read-only handle to one heap value (siblings `Box<T>`,
35/// `Arc<T>`).
36use std::rc::Rc;
37
38/// What:
39/// Why a scan pass stopped:
40/// the candidate layer failed,
41/// or the scanner did.
42/// Each variant carries the original typed failure.
43/// Why:
44/// Either way some file was not checked,
45/// so the pass has no result.
46/// Keeping the
47/// original failure lets the caller report its own cause.
48///
49/// In TS you'd write (pseudocode):
50/// ```ts
51/// type ScanRunError = { kind: 'candidate'; error: CandidateError } | { kind: 'scanner'; error: ScannerError };
52/// ```
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub enum ScanRunError {
55 /// Candidate bytes could not be read.
56 Candidate(CandidateError),
57 /// The scanner could not be given a candidate.
58 Scanner(ScannerError),
59}
60
61/// What:
62/// `impl std::fmt::Display for ScanRunError` supplies Rust's "print me" interface.
63/// Why:
64/// The message is the wrapped failure's own,
65/// unchanged.
66///
67/// In TS you'd write (pseudocode):
68/// ```ts
69/// toString(): string { return this.error.message; }
70/// ```
71impl std::fmt::Display for ScanRunError {
72 /// `&self` borrows this error read-only;
73 /// `&mut` lends the formatter for writing.
74 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 // `match self` reads the variant; each arm prints the message of the failure it carries.
76 match self {
77 ScanRunError::Candidate(error) => return formatter.write_str(error.message.as_str()),
78 ScanRunError::Scanner(error) => return formatter.write_str(error.message.as_str()),
79 }
80 }
81}
82
83/// An empty `impl` marks the type as a standard error value.
84impl std::error::Error for ScanRunError {}
85
86/// What:
87/// Where a scan pass reads candidate bytes.
88/// A `trait` is an interface.
89/// Why:
90/// The candidate store reads the version's objects;
91/// a direct fix reads the same
92/// objects with its in-memory corrections laid over them,
93/// and its later passes
94/// must scan the corrected bytes.
95///
96/// In TS you'd write (pseudocode):
97/// ```ts
98/// interface CandidateBytes { bytes(candidate: Candidate): Promise<Uint8Array> }
99/// ```
100pub trait CandidateBytes {
101 /// What:
102 /// The bytes the candidate holds for this reader.
103 /// Why:
104 /// the scan's only input.
105 ///
106 /// In TS you'd write (pseudocode):
107 /// ```ts
108 /// bytes(candidate: Candidate): Promise<Uint8Array>; // throws CandidateError
109 /// ```
110 fn candidate_bytes(&mut self, candidate: &Candidate) -> Result<Rc<[u8]>, CandidateError>;
111}
112
113/// What:
114/// `impl CandidateBytes for CandidateStore` reads through the store itself.
115/// Why:
116/// Without corrections the store's objects are the bytes to scan.
117///
118/// In TS you'd write (pseudocode):
119/// ```ts
120/// class CandidateStore implements CandidateBytes {}
121/// ```
122impl CandidateBytes for CandidateStore {
123 /// What:
124 /// The store's bytes for the candidate.
125 /// Why:
126 /// `CandidateStore::bytes` is the read.
127 ///
128 /// In TS you'd write (pseudocode):
129 /// ```ts
130 /// bytes(candidate) { return this.read(candidate); }
131 /// ```
132 fn candidate_bytes(&mut self, candidate: &Candidate) -> Result<Rc<[u8]>, CandidateError> {
133 return self.bytes(candidate);
134 }
135}
136
137/// What:
138/// Scan each eligible candidate and return the scanner's result for every one scanned.
139/// `&mut dyn CandidateBytes` lends any byte reader for changing (it may read objects);
140/// `&[Vec<u8>]` is "the rules files' candidate pathnames, none when no file is tracked";
141/// `Result<T, E>` is "a value or a failure".
142/// Why:
143/// Results are returned for clean candidates too,
144/// so the caller can tell
145/// "scanned and clean" from "not scanned".
146/// The first failure ends the pass with
147/// no partial result,
148/// because a partial result would look like a clean pass.
149///
150/// In TS you'd write (pseudocode):
151/// ```ts
152/// async function scanVersion(scanner, store, version, rulesPath?): Promise<CandidateScan[]>;
153/// ```
154pub fn scan_version(
155 scanner: &CandidateScanner,
156 store: &mut dyn CandidateBytes,
157 version: &CandidateVersion,
158 rules_paths: &[Vec<u8>],
159) -> Result<Vec<CandidateScan>, ScanRunError> {
160 return scan_version_skipping(scanner, store, version, rules_paths, &KnownContent::default());
161}
162
163/// What:
164/// `scan_version`,
165/// except that a candidate whose content a parent already holds is given to the scanner without
166/// bytes.
167/// `&KnownContent` lists those candidates by position.
168/// Why:
169/// The scanner reads the pathname of every candidate it is given,
170/// so a known candidate still has its
171/// new pathname checked,
172/// and its bytes are neither read from the store nor scanned.
173/// The result list
174/// keeps one entry per eligible candidate,
175/// so "scanned" and "skipped" differ only in the bytes behind
176/// the entry.
177///
178/// In TS you'd write (pseudocode):
179/// ```ts
180/// async function scanVersionSkipping(scanner, store, version, rulesPath, known): Promise<CandidateScan[]>;
181/// ```
182pub fn scan_version_skipping(
183 scanner: &CandidateScanner,
184 store: &mut dyn CandidateBytes,
185 version: &CandidateVersion,
186 rules_paths: &[Vec<u8>],
187 known: &KnownContent,
188) -> Result<Vec<CandidateScan>, ScanRunError> {
189 // `Vec::new()` is an empty growable list; `mut` lets the loop append to it.
190 let mut scans: Vec<CandidateScan> = Vec::new();
191 for candidate in version.candidates() {
192 if !is_scannable(candidate, rules_paths) {
193 continue;
194 }
195 // `.is_some()` is true when the candidate's content is already in a parent's tree.
196 if known.entry(candidate.identity.index).is_some() {
197 // An empty byte slice leaves the scanner only the pathname to check.
198 match scanner.scan(candidate, &[]) {
199 Ok(scan) => scans.push(scan),
200 Err(error) => return Err(ScanRunError::Scanner(error)),
201 }
202 continue;
203 }
204 // `match` on the read's `Result`: `Ok(read)` unwraps the bytes, `Err(error)` ends the pass.
205 let bytes: Rc<[u8]> = match store.candidate_bytes(candidate) {
206 Ok(read) => read,
207 // `Err(...)` is the failure variant.
208 Err(error) => return Err(ScanRunError::Candidate(error)),
209 };
210 // `&bytes` lends the shared bytes as a plain byte slice.
211 match scanner.scan(candidate, &bytes) {
212 Ok(scan) => scans.push(scan),
213 Err(error) => return Err(ScanRunError::Scanner(error)),
214 }
215 }
216 // `Ok(...)` is the success variant.
217 return Ok(scans);
218}
219
220#[cfg(test)]
221#[path = "scanner_run_tests.rs"]
222mod tests;
223
224/// Which candidates a pass skips,
225/// and how a pass ends on failure.
226#[cfg(test)]
227#[path = "scanner_run_selection_tests.rs"]
228mod selection_tests;