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 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;