Skip to main content

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