forbidden_strings/rule/frx/error.rs
1//! Redacted load-error type for the forbidden-regex rule compiler.
2//!
3//! Every variant is safe to print on stdout or stderr: it carries an opaque rule
4//! index (the rule's 0-based position in the compiled set, never a source line
5//! number) and the engine's own static reason, but never the rule text. This is
6//! the load-path half of the README's leak-safety guarantee (#217): a sensitive
7//! rule body may live in a CI secret without its bytes reaching CI logs.
8
9/// Imports the formatter pieces for the redacted `Display` rendering.
10use std::fmt;
11
12/// Imports the engine's compile-time error, surfaced as the static reason.
13use forbidden_regex::CompileError;
14
15/// A rule-load failure with everything the rule text redacted out.
16///
17/// The compiler fails closed on the first offending rule and reports only its
18/// opaque index plus a reason that echoes no pattern bytes. `Debug` is derived
19/// because no variant holds rule text, so the derived form is leak-safe too.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum LoadError {
22 /// The source held no non-blank, non-comment rule line.
23 NoRules,
24 /// A regex rule carried a flag letter other than the `m`/`x` no-ops.
25 UnsupportedFlag {
26 /// Opaque 0-based index of the offending rule in the compiled set.
27 index: usize,
28 /// First flag letter outside `{m, x}`; a config letter, never rule text.
29 flag: char,
30 },
31 /// The engine rejected a rule at compile time (bad dialect, empty-matchable,
32 /// oversized repetition, state-cap blowup).
33 Compile {
34 /// Opaque 0-based index of the offending rule in the compiled set.
35 index: usize,
36 /// Engine's static reason; its `Display` echoes no pattern bytes.
37 reason: CompileError,
38 },
39 /// A precompiled serialized `RegexSet` blob failed to decode or validate.
40 Precompiled {
41 /// Engine's static reason; a codec/validation message, never rule text.
42 reason: CompileError,
43 },
44 /// A tail-format line's trimmed form led with `==>` without being exactly a
45 /// strict `==> name <==` header, so it is a near-header, never absorbed
46 /// silently into a section body; genuine arrow-leading content uses the
47 /// `[=]=> ` reshape in regex bodies.
48 NearHeader {
49 /// 1-based source line of the malformed header; carries no name text.
50 line: usize,
51 },
52 /// A tail-format file carried significant content before its first section
53 /// header, which the format forbids so no rule can escape a section.
54 PreHeaderContent {
55 /// 1-based source line of the offending pre-header content.
56 line: usize,
57 },
58 /// A tail-format section header opened a body with no significant rule line,
59 /// which is fail-closed rather than a silently dropped section.
60 EmptySection {
61 /// 1-based source line of the empty section's header.
62 line: usize,
63 },
64 /// Two tail-format sections declared the same name, which collides the rule
65 /// identities the format exists to keep unique across the loaded input.
66 DuplicateName {
67 /// 1-based source line where the name was first declared.
68 first_line: usize,
69 /// 1-based source line of the colliding redeclaration.
70 line: usize,
71 },
72}
73
74/// Renders a `LoadError` as a redacted, user-facing diagnostic.
75impl fmt::Display for LoadError {
76 /// Writes a one-line reason that never contains rule text.
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 // Branch per variant; each interpolates only an opaque index, a config
79 // flag letter, or the engine's own static reason.
80 match self {
81 LoadError::NoRules => {
82 return write!(f, "no rules loaded")
83 }
84 LoadError::UnsupportedFlag { index, flag } => {
85 return write!(
86 f,
87 "rule {index}: unsupported flag '{flag}'; only 'm' and 'x' are accepted as no-ops",
88 )
89 }
90 LoadError::Compile { index, reason } => {
91 return write!(f, "rule {index}: {reason}")
92 }
93 LoadError::Precompiled { reason } => {
94 return write!(f, "precompiled ruleset failed to load: {reason}")
95 }
96 LoadError::NearHeader { line } => {
97 return write!(
98 f,
99 "line {line}: line starts with '==>' but is not a strict '==> name <==' section header; reshape genuine content as '[=]=>'",
100 )
101 }
102 LoadError::PreHeaderContent { line } => {
103 return write!(f, "line {line}: content before the first section header")
104 }
105 LoadError::EmptySection { line } => {
106 return write!(f, "line {line}: section header has no rule body")
107 }
108 LoadError::DuplicateName { first_line, line } => {
109 return write!(
110 f,
111 "line {line}: duplicate section name first declared at line {first_line}",
112 )
113 }
114 }
115 }
116}
117
118/// Lets `LoadError` participate in the standard error ecosystem.
119impl std::error::Error for LoadError {}
120
121/// Registers the redaction and rendering unit tests (sidecar, lint-exempt).
122#[cfg(test)]
123#[path = "error_tests.rs"]
124mod error_tests;