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/// Native paths preserve operating-system encoding until matching/display boundaries.
26use std::path::Path;
27
28/// What:
29/// Caller-owned identity plus safely displayable results from one exact candidate snapshot.
30/// Why:
31/// Matching pathname components may have identical masked labels;
32/// identity must remain independent of display text.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// type CandidateScan = { identity: number; displayPath: string; findings: ScanFinding[]; scannedBytes: number };
37/// ```
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct CandidateScan {
40 /// Opaque identity chosen by the caller,
41 /// not derived from any possibly redacted pathname.
42 pub identity: usize,
43 /// Logical pathname with matching components masked and protocol-sensitive characters escaped.
44 pub display_path: String,
45 /// Structured content/name matches and explicit processing failures.
46 pub findings: Vec<ScanFinding>,
47 /// Number of bytes inspected after applying the existing binary-prefix policy.
48 pub scanned_bytes: usize,
49}
50
51/// One loaded ruleset reused across candidate snapshots.
52///
53/// Embedding hosts must use panic=unwind and configure payload-free panic output before spawning workers.
54/// The library catches load/matcher unwinds but deliberately does not replace process-wide hooks;
55/// Rust invokes the host's hook before an unwind can be caught.
56pub struct Scanner {
57 /// The incumbent loader retains hybrid runtime rules,
58 /// embedded baseline and cache warnings.
59 loaded: LoadedRules,
60}
61
62/// Load once and scan exact snapshots without reading candidate files from the live worktree.
63impl Scanner {
64 /// What:
65 /// Load runtime rules and the optional shipped baseline through the incumbent cache/validation path.
66 /// Why:
67 /// The embedding adapter does not create a second compiler,
68 /// cache or builtin-rules loader.
69 ///
70 /// In TS you'd write (pseudocode):
71 /// ```ts
72 /// function loadScanner(runtimeRulesPath, builtinRules, explicitlySelectedPath): Scanner;
73 /// ```
74 pub fn load(runtime_rules_path: &Path, builtin_rules: bool, explicit: bool) -> Result<Scanner> {
75 // Propagate the loader's existing redacted failure instead of accepting an incomplete ruleset.
76 let loaded: LoadedRules = load_request::load(runtime_rules_path, builtin_rules, explicit)?;
77 return Ok(Scanner { loaded });
78 }
79
80 /// Borrow fixed-token cache warnings without recovering their fields from serialized terminal text.
81 pub fn cache_warnings(&self) -> &[CacheWarning] {
82 return self.loaded.cache_warnings();
83 }
84
85 /// What:
86 /// Scan a candidate's logical pathname and exact immutable bytes.
87 /// Why:
88 /// Historical/index candidates must never substitute the current worktree file or its temporary materialization name.
89 ///
90 /// In TS you'd write (pseudocode):
91 /// ```ts
92 /// function scan(identity, logicalPath, exactBytes): CandidateScan;
93 /// ```
94 pub fn scan(&self, identity: usize, logical_path: &Path, bytes: &[u8]) -> CandidateScan {
95 // Canonical pathname records own a safe display path before content diagnostics are attached.
96 let pathname: PathScanRecords = scan_path_records(logical_path, &self.loaded);
97 // Mirror the standalone reader: binary files retain their first 8 KiB, not an invented clean/skip result.
98 let probe_length: usize = bytes.len().min(BIN_PROBE_SIZE);
99 let probe: &[u8] = &bytes[..probe_length];
100 let content: &[u8] = if memchr::memchr(0, probe).is_some() { probe } else { bytes };
101 let mut findings: Vec<ScanFinding> = pathname.findings;
102 findings.extend(scan_content(content, &self.loaded));
103 return CandidateScan {
104 identity,
105 display_path: pathname.display,
106 findings,
107 scanned_bytes: content.len(),
108 };
109 }
110}
111
112/// Compile a cache-free scanner through the production hybrid matcher without filesystem or environment state.
113#[cfg(feature = "fuzzing")]
114pub fn scanner_from_text_for_fuzzing(text: &str) -> Result<Scanner> {
115 // Propagate the existing redacted compiler failure; never construct a partial scanner.
116 let loaded: LoadedRules = crate::frx_load::hybrid_from_text(text)?;
117 return Ok(Scanner { loaded });
118}
119
120/// Embedded-consumer controls use cache-free,
121/// disposable rule fixtures.
122#[cfg(test)]
123#[path = "scanner_tests.rs"]
124mod tests;
125
126/// Prefix edges and caught-panic reuse controls remain separate from the normal snapshot examples.
127#[cfg(test)]
128#[path = "scanner_boundary_tests.rs"]
129mod boundary_tests;