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