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;