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