Skip to main content

forbidden_strings/rule/
frx.rs

1//! Rule compiler targeting the in-house `forbidden-regex` engine.
2//!
3//! This module owns the two-form rule-file format, the flag policy, UTF-8 BOM
4//! stripping, redacted load-error reporting, and both construction paths (from
5//! rule text and from a serialized precompiled `RegexSet`). It is the live load
6//! path: `crate::frx_load` builds the scan's rule sets through these entry points,
7//! and `crate::frx_scan` runs them. The resharp/`regex`-crate pipeline it once sat
8//! beside was deleted in the engine-swap teardown (#385).
9//!
10//! The engine is always in verbose, multiline mode, so this compiler never calls
11//! `forbidden_regex::compile` (its single-pattern path logs the pattern via
12//! `tracing`, a leak vector). It builds only through `RegexSet::new` and
13//! `RegexSet::from_bytes`, neither of which logs, so a failing rule's bytes never
14//! reach a subscriber.
15
16/// Imports the engine's compiled-ruleset type and its compile-time error.
17use forbidden_regex::{CompileError, RegexSet};
18
19/// Registers the redacted load-error type.
20mod error;
21/// Registers the literal-to-verbose-dialect escaper.
22mod escape;
23/// Registers the format autodetector, legacy line parser, and flag policy.
24mod format;
25/// Registers the tail-format sectioned parser and header grammar.
26mod sections;
27
28/// Re-exports the redacted load-error type as this module's public failure.
29pub use error::LoadError;
30
31/// Re-exports the literal-to-verbose-dialect escaper for the scanner's fuzz targets.
32///
33/// The `fuzz_literal_roundtrip` target drives this escaper directly (not through the
34/// two-form format layer, whose `.trim()`, comment, and regex classification would
35/// eat the adversarial cases it must exercise: leading `#`, embedded newlines, a
36/// leading `/`). Gated on `fuzzing` so the production surface stays unchanged.
37#[cfg(feature = "fuzzing")]
38pub use escape::escape_literal;
39
40/// One compiled rule set paired with the per-rule names that drive finding identity.
41///
42/// `names` is parallel to the set's rule indices: a tail-format source names every
43/// rule (its section name, rendered in findings as `rule=<name>`), a legacy source
44/// names none (findings fall back to the offset numeric index). Splitting the two
45/// out of the parse keeps the engine set free of identity concerns.
46pub struct CompiledRules {
47    /// Compiled engine set the scan path runs.
48    pub set: RegexSet,
49    /// Per-rule section names parallel to the set's rule indices; `None` for
50    /// legacy line-based rules.
51    pub names: Vec<Option<String>>,
52}
53
54/// Runtime rule kind retained across parser and hybrid matcher seam.
55pub(crate) enum RuntimeRuleKind {
56    /// Original exact literal bytes before dialect escaping.
57    ExactLiteral(
58        /// Exact source bytes consumed by direct literal matcher.
59        Vec<u8>,
60    ),
61    /// Engine-ready restricted-regex dialect pattern.
62    RestrictedRegex(
63        /// Parsed dialect pattern consumed by in-house regex engine.
64        String,
65    ),
66}
67
68/// Parsed runtime rule with finding identity and pre-escape kind.
69pub(crate) struct RuntimeRuleInput {
70    /// Optional tail-format section identity.
71    pub(crate) name: Option<String>,
72    /// Exact-literal or restricted-regex matcher input.
73    pub(crate) kind: RuntimeRuleKind,
74}
75
76/// Parses runtime source while preserving bare-literal distinction.
77pub(crate) fn parse_runtime_rules(
78    text: &str,
79) -> Result<Vec<RuntimeRuleInput>, LoadError> {
80    let rules = format::parse_rules(text)?;
81    return Ok(rules
82        .into_iter()
83        .map(|rule| {
84            let kind = if let Some(literal) = rule.literal {
85                RuntimeRuleKind::ExactLiteral(literal)
86            } else {
87                RuntimeRuleKind::RestrictedRegex(rule.pattern)
88            };
89            return RuntimeRuleInput { name: rule.name, kind }
90        })
91        .collect())
92}
93
94/// Compiles a rule source into a combined `RegexSet` plus its per-rule names.
95///
96/// Parses the autodetected format (escaping literals, applying the flag policy,
97/// stripping a BOM), then validates each rule through the engine to attribute a
98/// redacted, index-bearing error to the first offender, and finally assembles the
99/// combined set. Validation and assembly both go through `RegexSet::new`, so no
100/// pattern is ever logged. Errors carry only an opaque rule index and the
101/// engine's static reason, never rule text.
102pub fn compile_rules(text: &str) -> Result<CompiledRules, LoadError> {
103    let rules = format::parse_rules(text)?;
104    let (names, patterns): (Vec<Option<String>>, Vec<String>) =
105        rules.into_iter().map(|rule| return (rule.name, rule.pattern)).unzip();
106    // Validate rule by rule first: `RegexSet::new` over the whole slice reports
107    // only its first error without the offending index, so a per-rule pass
108    // recovers the index for the redacted diagnostic. Each single-rule set is
109    // discarded; the combined set is built once below.
110    for (index, pattern) in patterns.iter().enumerate() {
111        if let Err(reason) = RegexSet::new(std::slice::from_ref(pattern)) {
112            return Err(LoadError::Compile { index, reason });
113        }
114    }
115    // Every rule compiled individually, so assembly cannot fail on a rule; any
116    // error here is a genuine engine invariant break, surfaced fail-closed with a
117    // sentinel index and the engine's static reason (still no rule text).
118    let set = RegexSet::new(&patterns).map_err(|reason| return LoadError::Compile {
119        index: patterns.len(),
120        reason,
121    })?;
122    return Ok(CompiledRules { set, names });
123}
124
125/// Compiles a rule source (autodetected format) into a combined `RegexSet`.
126///
127/// A projection over [`compile_rules`] for callers that only need the engine set
128/// (the build-time baseline verifier, the fuzz targets); the runtime loader uses
129/// [`compile_rules`] so findings can carry rule names.
130pub fn compile_from_text(text: &str) -> Result<RegexSet, LoadError> {
131    return compile_rules(text).map(|compiled| return compiled.set);
132}
133
134/// Loads a precompiled serialized `RegexSet` from bytes.
135///
136/// Wraps the engine's `from_bytes`, which decodes and structurally validates the
137/// blob before it can match. The planning doc resolved that the builtin baseline
138/// is embedded precompiled at build time (runtime compilation is too slow); stage
139/// two performs the embedding, and this is the construction path it will use. A
140/// decode or validation failure becomes a redacted `Precompiled` error.
141pub fn load_precompiled(bytes: &[u8]) -> Result<RegexSet, LoadError> {
142    return RegexSet::from_bytes(bytes)
143        .map_err(|reason: CompileError| return LoadError::Precompiled { reason });
144}
145
146/// Registers the compile, redaction, and precompiled round-trip tests
147/// (sidecar, lint-exempt).
148#[cfg(test)]
149mod compile_tests;