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: 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;