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