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;