forbidden_strings/scanner.rs
1//! What:
2//! In-process scanning of caller-owned candidate snapshots.
3//! Why:
4//! cli-git must scan the selected version's exact bytes and logical name without temporary content files or text parsing.
5//!
6//! In TS you'd write (pseudocode):
7//! ```ts
8//! // Load one scanner, then scan immutable candidate identities, names and bytes.
9//! ```
10
11/// Import the existing loader's rule sets that one scanner owns.
12use crate::frx_load::LoadedRules;
13/// The loader's named unwind boundary returns no partial ruleset after a panic.
14use crate::load_request;
15/// Imports the content matcher that reports line findings for one candidate's bytes.
16use crate::frx_scan::scan_content;
17/// Imports the pathname masker and its display-plus-findings result.
18use crate::path_scan::{PathScanRecords, scan_path_records};
19/// Imports the binary-probe length and the structured finding model returned to callers.
20use crate::{BIN_PROBE_SIZE, ScanFinding};
21/// Import the incumbent load-error channel.
22use anyhow::Result;
23/// Imports the fixed-token cache diagnostic retained from loading.
24use crate::runtime_cache::CacheWarning;
25/// Imports the list entry a host passes to name its rules files.
26use crate::rules_list::RulesEntry;
27/// Native paths preserve operating-system encoding until matching/display boundaries.
28use std::path::{Path, PathBuf};
29
30/// What:
31/// Caller-owned identity plus safely displayable results from one exact candidate snapshot.
32/// Why:
33/// Matching pathname components may have identical masked labels;
34/// identity must remain independent of display text.
35///
36/// In TS you'd write (pseudocode):
37/// ```ts
38/// type CandidateScan = { identity: number; displayPath: string; findings: ScanFinding[]; scannedBytes: number };
39/// ```
40#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct CandidateScan {
42 /// Opaque identity chosen by the caller,
43 /// not derived from any possibly redacted pathname.
44 pub identity: usize,
45 /// Logical pathname with matching components masked and protocol-sensitive characters escaped.
46 pub display_path: String,
47 /// Structured content/name matches and explicit processing failures.
48 pub findings: Vec<ScanFinding>,
49 /// Number of bytes inspected after applying the existing binary-prefix policy.
50 pub scanned_bytes: usize,
51}
52
53/// One loaded ruleset reused across candidate snapshots.
54///
55/// Embedding hosts must use panic=unwind and configure payload-free panic output before spawning workers.
56/// The library catches load/matcher unwinds but deliberately does not replace process-wide hooks;
57/// Rust invokes the host's hook before an unwind can be caught.
58pub struct Scanner {
59 /// The incumbent loader retains hybrid runtime rules,
60 /// embedded baseline and cache warnings.
61 loaded: LoadedRules,
62}
63
64/// Load once and scan exact snapshots without reading candidate files from the live worktree.
65impl Scanner {
66 /// What:
67 /// Load runtime rules and the optional shipped baseline through the incumbent cache/validation path.
68 /// Why:
69 /// The embedding adapter does not create a second compiler,
70 /// cache or builtin-rules loader.
71 ///
72 /// In TS you'd write (pseudocode):
73 /// ```ts
74 /// function loadScanner(runtimeRulesPath, builtinRules, explicitlySelectedPath): Scanner;
75 /// ```
76 pub fn load(runtime_rules_path: &Path, builtin_rules: bool, explicit: bool) -> Result<Scanner> {
77 // Propagate the loader's existing redacted failure instead of accepting an incomplete ruleset.
78 let loaded: LoadedRules = load_request::load(runtime_rules_path, builtin_rules, explicit)?;
79 return Ok(Scanner { loaded });
80 }
81
82 /// What:
83 /// Load the files of a rules list and the optional shipped baseline through the same cache/validation path.
84 /// Why:
85 /// A host configured with several rules files (some optional) needs one scanner over their joined rules;
86 /// the one-path [`Scanner::load`] stays for hosts that name one file.
87 /// A required entry that cannot be read fails the load naming its path,
88 /// and an absent optional entry is skipped and listed by [`Scanner::skipped_optional_rules`].
89 ///
90 /// In TS you'd write (pseudocode):
91 /// ```ts
92 /// function loadScannerFromList(entries: { path: string; optional: boolean }[], builtinRules: boolean): Scanner;
93 /// ```
94 pub fn load_entries(entries: &[RulesEntry], builtin_rules: bool) -> Result<Scanner> {
95 // Propagate the loader's existing redacted failure instead of accepting an incomplete ruleset.
96 let loaded: LoadedRules = load_request::load_entries(entries, builtin_rules)?;
97 return Ok(Scanner { loaded });
98 }
99
100 /// Borrow the optional rules files that were absent and skipped,
101 /// for hosts without a log subscriber to report.
102 pub fn skipped_optional_rules(&self) -> &[PathBuf] {
103 return self.loaded.skipped_optional();
104 }
105
106 /// Borrow fixed-token cache warnings without recovering their fields from serialized terminal text.
107 pub fn cache_warnings(&self) -> &[CacheWarning] {
108 return self.loaded.cache_warnings();
109 }
110
111 /// What:
112 /// Scan a candidate's logical pathname and exact immutable bytes.
113 /// Why:
114 /// Historical/index candidates must never substitute the current worktree file or its temporary materialization name.
115 ///
116 /// In TS you'd write (pseudocode):
117 /// ```ts
118 /// function scan(identity, logicalPath, exactBytes): CandidateScan;
119 /// ```
120 pub fn scan(&self, identity: usize, logical_path: &Path, bytes: &[u8]) -> CandidateScan {
121 // Canonical pathname records own a safe display path before content diagnostics are attached.
122 let pathname: PathScanRecords = scan_path_records(logical_path, &self.loaded);
123 // Mirror the standalone reader: binary files retain their first 8 KiB, not an invented clean/skip result.
124 let probe_length: usize = bytes.len().min(BIN_PROBE_SIZE);
125 let probe: &[u8] = &bytes[..probe_length];
126 let content: &[u8] = if memchr::memchr(0, probe).is_some() { probe } else { bytes };
127 let mut findings: Vec<ScanFinding> = pathname.findings;
128 findings.extend(scan_content(content, &self.loaded));
129 return CandidateScan {
130 identity,
131 display_path: pathname.display,
132 findings,
133 scanned_bytes: content.len(),
134 };
135 }
136}
137
138/// Compile a cache-free scanner through the production hybrid matcher without filesystem or environment state.
139#[cfg(feature = "fuzzing")]
140pub fn scanner_from_text_for_fuzzing(text: &str) -> Result<Scanner> {
141 // Propagate the existing redacted compiler failure; never construct a partial scanner.
142 let loaded: LoadedRules = crate::frx_load::hybrid_from_text(text)?;
143 return Ok(Scanner { loaded });
144}
145
146/// Embedded-consumer controls use cache-free,
147/// disposable rule fixtures.
148#[cfg(test)]
149#[path = "scanner_tests.rs"]
150mod tests;
151
152/// Prefix edges and caught-panic reuse controls remain separate from the normal snapshot examples.
153#[cfg(test)]
154#[path = "scanner_boundary_tests.rs"]
155mod boundary_tests;