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;