git_policy_cli/scanner_failure_code.rs
1//! What:
2//! Which engine failure code each failure of the candidate and scanner layers carries.
3//! Why:
4//! An engine failure names its cause.
5//! `content-unavailable` means candidate bytes or a
6//! repository fact could not be read;
7//! `policy-incomplete` means the policy's own
8//! machinery failed.
9//! Deciding the code here,
10//! beside the failures,
11//! makes every policy
12//! that reads candidates or scans them report the same code for the same cause.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! // const code = scanRunFailureCode(error); // 'content-unavailable' | 'policy-incomplete'
17//! ```
18
19/// Import the candidate layer's closed list of causes.
20use super::candidate_error::CandidateFailure;
21/// Import the engine's failure codes;
22/// this module only chooses among them.
23use super::diagnostics::EngineFailureCode;
24/// Import the adapter's closed list of causes.
25use super::scanner_adapter::ScannerFailure;
26/// Import the failure of a whole scan pass.
27use super::scanner_run::ScanRunError;
28/// The scanner library's per-match result,
29/// which also carries its fail-closed notices.
30use forbidden_strings::ScanFinding;
31
32/// What:
33/// The code of one candidate-layer failure.
34/// `match` lists every cause by name and
35/// has no catch-all arm;
36/// `A | B` in an arm means "either of these".
37/// Why:
38/// Every cause but one is something Git could not list or hand over,
39/// or a private
40/// index a prediction needed could not be prepared,
41/// so the content was unavailable.
42/// A stale candidate is different:
43/// its content can be read through
44/// a fresh version,
45/// and what failed is the calling pass,
46/// which kept a candidate
47/// across an invalidation.
48/// Without a catch-all arm,
49/// a cause added later does not
50/// compile until someone chooses its code.
51///
52/// In TS you'd write (pseudocode):
53/// ```ts
54/// function candidateFailureCode(failure: CandidateFailure): EngineFailureCode;
55/// ```
56pub fn candidate_failure_code(failure: CandidateFailure) -> EngineFailureCode {
57 match failure {
58 CandidateFailure::GitNotStarted
59 | CandidateFailure::GitFailed
60 | CandidateFailure::ListingMalformed
61 | CandidateFailure::UnsupportedMode
62 | CandidateFailure::UnsupportedStatus
63 | CandidateFailure::UnmergedPath
64 | CandidateFailure::ReaderEnded
65 | CandidateFailure::ReplyMalformed
66 | CandidateFailure::ReplyTruncated
67 | CandidateFailure::ReplyMismatched
68 | CandidateFailure::ObjectMissing
69 | CandidateFailure::ObjectKindUnexpected
70 | CandidateFailure::PrivateIndexUnavailable => {
71 return EngineFailureCode::ContentUnavailable;
72 }
73 CandidateFailure::StaleCandidate => return EngineFailureCode::PolicyIncomplete,
74 }
75}
76
77/// What:
78/// The code of one adapter failure.
79/// Why:
80/// Rules that cannot be loaded,
81/// and a pathname that was read but cannot be handed
82/// to the scanner on this platform,
83/// are both failures of the policy's machinery;
84/// no repository content was unreadable.
85/// The `match` names both causes so a cause
86/// added later does not compile until someone chooses its code.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// function scannerFailureCode(failure: ScannerFailure): EngineFailureCode;
91/// ```
92pub fn scanner_failure_code(failure: ScannerFailure) -> EngineFailureCode {
93 match failure {
94 ScannerFailure::RulesNotLoaded | ScannerFailure::PathnameUnrepresentable => {
95 return EngineFailureCode::PolicyIncomplete;
96 }
97 }
98}
99
100/// What:
101/// The code of a failed scan pass.
102/// `&ScanRunError` borrows the failure read-only.
103/// Why:
104/// A pass fails in the candidate layer or in the adapter,
105/// and each already has its
106/// code;
107/// the pass reports the code of the failure it wraps.
108///
109/// In TS you'd write (pseudocode):
110/// ```ts
111/// function scanRunFailureCode(error: ScanRunError): EngineFailureCode;
112/// ```
113pub fn scan_run_failure_code(error: &ScanRunError) -> EngineFailureCode {
114 match error {
115 ScanRunError::Candidate(candidate) => return candidate_failure_code(candidate.failure),
116 ScanRunError::Scanner(scanner) => return scanner_failure_code(scanner.failure),
117 }
118}
119
120/// What:
121/// The code of a finding that is not a violation,
122/// or nothing for a violation.
123/// `Option<EngineFailureCode>` is "a code or nothing";
124/// `{ .. }` ignores a variant's fields.
125/// Why:
126/// The scanner reports two things among its findings that are not matches:
127/// a matcher
128/// that failed,
129/// and a pathname with a line break that it could not inspect.
130/// Neither
131/// says the candidate is clean and neither names a rule,
132/// so each must end the pass
133/// as an engine failure instead of being printed as a rule violation.
134/// Both arrive
135/// after the candidate was read,
136/// so the cause is the policy's machinery.
137///
138/// In TS you'd write (pseudocode):
139/// ```ts
140/// function findingFailureCode(finding: ScanFinding): EngineFailureCode | undefined;
141/// ```
142pub fn finding_failure_code(finding: &ScanFinding) -> Option<EngineFailureCode> {
143 match finding {
144 // `None` is the "absent" variant: a content or name match is a violation, not a failure.
145 ScanFinding::Content { .. } | ScanFinding::Name { .. } => return None,
146 // `Some(...)` is the "present" variant.
147 ScanFinding::EngineError | ScanFinding::PathnameLineBreak => {
148 return Some(EngineFailureCode::PolicyIncomplete);
149 }
150 }
151}
152
153/// One control per mapping,
154/// over every variant.
155#[cfg(test)]
156#[path = "scanner_failure_code_tests.rs"]
157mod tests;