git_policy_cli/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 super::candidate_error::CandidateError;
22use super::candidate_store::CandidateStore;
23use super::candidate_version::{Candidate, CandidateVersion};
24/// Import the loaded scanner and its failure.
25use super::scanner_adapter::{CandidateScanner, ScannerError};
26/// Import the eligibility decision.
27use super::scanner_selection::is_scannable;
28/// The scanner library's per-candidate result,
29/// returned unchanged.
30use forbidden_strings::CandidateScan;
31/// `Rc<T>` is a shared,
32/// read-only handle to one heap value (siblings `Box<T>`,
33/// `Arc<T>`).
34use std::rc::Rc;
35
36/// What:
37/// Why a scan pass stopped:
38/// the candidate layer failed,
39/// or the scanner did.
40/// Each variant carries the original typed failure.
41/// Why:
42/// Either way some file was not checked,
43/// so the pass has no result.
44/// Keeping the
45/// original failure lets the caller report its own cause.
46///
47/// In TS you'd write (pseudocode):
48/// ```ts
49/// type ScanRunError = { kind: 'candidate'; error: CandidateError } | { kind: 'scanner'; error: ScannerError };
50/// ```
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub enum ScanRunError {
53 /// Candidate bytes could not be read.
54 Candidate(CandidateError),
55 /// The scanner could not be given a candidate.
56 Scanner(ScannerError),
57}
58
59/// What:
60/// `impl std::fmt::Display for ScanRunError` supplies Rust's "print me" interface.
61/// Why:
62/// The message is the wrapped failure's own,
63/// unchanged.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// toString(): string { return this.error.message; }
68/// ```
69impl std::fmt::Display for ScanRunError {
70 /// `&self` borrows this error read-only;
71 /// `&mut` lends the formatter for writing.
72 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 // `match self` reads the variant; each arm prints the message of the failure it carries.
74 match self {
75 ScanRunError::Candidate(error) => return formatter.write_str(error.message.as_str()),
76 ScanRunError::Scanner(error) => return formatter.write_str(error.message.as_str()),
77 }
78 }
79}
80
81/// An empty `impl` marks the type as a standard error value.
82impl std::error::Error for ScanRunError {}
83
84/// What:
85/// Where a scan pass reads candidate bytes.
86/// A `trait` is an interface.
87/// Why:
88/// The candidate store reads the version's objects;
89/// a direct fix reads the same
90/// objects with its in-memory corrections laid over them,
91/// and its later passes
92/// must scan the corrected bytes.
93///
94/// In TS you'd write (pseudocode):
95/// ```ts
96/// interface CandidateBytes { bytes(candidate: Candidate): Promise<Uint8Array> }
97/// ```
98pub trait CandidateBytes {
99 /// What:
100 /// The bytes the candidate holds for this reader.
101 /// Why:
102 /// the scan's only input.
103 ///
104 /// In TS you'd write (pseudocode):
105 /// ```ts
106 /// bytes(candidate: Candidate): Promise<Uint8Array>; // throws CandidateError
107 /// ```
108 fn candidate_bytes(&mut self, candidate: &Candidate) -> Result<Rc<[u8]>, CandidateError>;
109}
110
111/// What:
112/// `impl CandidateBytes for CandidateStore` reads through the store itself.
113/// Why:
114/// Without corrections the store's objects are the bytes to scan.
115///
116/// In TS you'd write (pseudocode):
117/// ```ts
118/// class CandidateStore implements CandidateBytes {}
119/// ```
120impl CandidateBytes for CandidateStore {
121 /// What:
122 /// The store's bytes for the candidate.
123 /// Why:
124 /// `CandidateStore::bytes` is the read.
125 ///
126 /// In TS you'd write (pseudocode):
127 /// ```ts
128 /// bytes(candidate) { return this.read(candidate); }
129 /// ```
130 fn candidate_bytes(&mut self, candidate: &Candidate) -> Result<Rc<[u8]>, CandidateError> {
131 return self.bytes(candidate);
132 }
133}
134
135/// What:
136/// Scan each eligible candidate and return the scanner's result for every one scanned.
137/// `&mut dyn CandidateBytes` lends any byte reader for changing (it may read objects);
138/// `Option<&[u8]>` is "the rules file's candidate pathname or nothing";
139/// `Result<T, E>` is "a value or a failure".
140/// Why:
141/// Results are returned for clean candidates too,
142/// so the caller can tell
143/// "scanned and clean" from "not scanned".
144/// The first failure ends the pass with
145/// no partial result,
146/// because a partial result would look like a clean pass.
147///
148/// In TS you'd write (pseudocode):
149/// ```ts
150/// async function scanVersion(scanner, store, version, rulesPath?): Promise<CandidateScan[]>;
151/// ```
152pub fn scan_version(
153 scanner: &CandidateScanner,
154 store: &mut dyn CandidateBytes,
155 version: &CandidateVersion,
156 rules_path: Option<&[u8]>,
157) -> Result<Vec<CandidateScan>, ScanRunError> {
158 // `Vec::new()` is an empty growable list; `mut` lets the loop append to it.
159 let mut scans: Vec<CandidateScan> = Vec::new();
160 for candidate in version.candidates() {
161 if !is_scannable(candidate, rules_path) {
162 continue;
163 }
164 // `match` on the read's `Result`: `Ok(read)` unwraps the bytes, `Err(error)` ends the pass.
165 let bytes: Rc<[u8]> = match store.candidate_bytes(candidate) {
166 Ok(read) => read,
167 // `Err(...)` is the failure variant.
168 Err(error) => return Err(ScanRunError::Candidate(error)),
169 };
170 // `&bytes` lends the shared bytes as a plain byte slice.
171 match scanner.scan(candidate, &bytes) {
172 Ok(scan) => scans.push(scan),
173 Err(error) => return Err(ScanRunError::Scanner(error)),
174 }
175 }
176 // `Ok(...)` is the success variant.
177 return Ok(scans);
178}
179
180/// Isolation controls stay out of the release executable.
181#[cfg(test)]
182#[path = "scanner_run_tests.rs"]
183mod tests;
184
185/// Which candidates a pass skips,
186/// and how a pass ends on failure.
187#[cfg(test)]
188#[path = "scanner_run_selection_tests.rs"]
189mod selection_tests;