Skip to main content

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 candidates whose content a parent already holds.
25use super::candidate_known_content::KnownContent;
26/// Import the loaded scanner and its failure.
27use super::scanner_adapter::{CandidateScanner, ScannerError};
28/// Import the eligibility decision.
29use super::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;