Skip to main content

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;