rusty_figlet/error.rs
1//! Library error type for `rusty-figlet`.
2//!
3//! [`FigletError`] is the unified error type returned by every fallible
4//! public API in this crate. It is marked `#[non_exhaustive]` so that
5//! additive variants in future releases remain non-breaking under SemVer
6//! (per AD-013). Downstream consumers that pattern-match on the enum MUST
7//! include a wildcard `_` arm.
8//!
9//! `Send + Sync + 'static` is guaranteed at compile time (SC-009) so the
10//! error works across async `await` points and thread boundaries.
11
12use std::io;
13use std::path::PathBuf;
14
15/// All fallible operations in `rusty-figlet` return `Result<T, FigletError>`.
16///
17/// The enum is `#[non_exhaustive]` (per AD-013) so additive variants in
18/// future minor releases do NOT constitute a breaking change. Downstream
19/// matches MUST include a wildcard `_` arm:
20///
21/// ```rust
22/// use rusty_figlet::FigletError;
23/// fn describe(err: &FigletError) -> &'static str {
24/// match err {
25/// FigletError::FontNotFound { .. } => "missing font",
26/// FigletError::FontParse { .. } => "bad font file",
27/// FigletError::Io(_) => "io error",
28/// FigletError::WidthTooNarrow { .. } => "width too narrow",
29/// FigletError::UnknownFilter { .. } => "unknown filter",
30/// FigletError::Internal(_) => "internal error",
31/// _ => "unknown",
32/// }
33/// }
34/// ```
35///
36/// `Error::source()` returns `Some(&io::Error)` ONLY for the [`FigletError::Io`]
37/// variant; all other variants are leaf errors and return `None` from
38/// `source()`. `FontParse { line }` is 1-indexed and matches the convention
39/// used by upstream `figlet(6)` parse-error stderr messages.
40#[non_exhaustive]
41#[derive(Debug, thiserror::Error)]
42pub enum FigletError {
43 /// The requested font name (or path) could not be located.
44 ///
45 /// `name` is the user-supplied identifier; `searched` is the ordered
46 /// list of paths the resolver consulted, suitable for displaying in a
47 /// diagnostic message.
48 #[error("font not found: {name}; searched {searched:?}")]
49 FontNotFound {
50 /// Font name or path the user supplied (e.g. `"slant"`, `"./my.flf"`).
51 name: String,
52 /// Ordered list of paths inspected during font resolution.
53 searched: Vec<PathBuf>,
54 },
55
56 /// A `.flf` file failed to parse.
57 ///
58 /// `reason` is a short human description (e.g. `"bad signature"`,
59 /// `"missing endmark"`); `line` is the 1-indexed line number at which
60 /// the parser detected the problem.
61 #[error("font parse error at line {line}: {reason}")]
62 FontParse {
63 /// Short human-readable description of the parse failure.
64 reason: String,
65 /// 1-indexed line number where the parse error was detected.
66 line: u32,
67 },
68
69 /// Underlying I/O failure (file read, stdin, stdout).
70 ///
71 /// `Error::source()` returns the wrapped [`io::Error`] for this variant.
72 #[error("io error: {0}")]
73 Io(#[from] io::Error),
74
75 /// The requested width is too narrow to render the requested glyph(s).
76 ///
77 /// `needed` is the minimum width a single glyph requires; `given` is
78 /// the width the caller supplied.
79 #[error("width too narrow: needed {needed}, given {given}")]
80 WidthTooNarrow {
81 /// Minimum width required by the widest glyph.
82 needed: u32,
83 /// Width supplied by the caller.
84 given: u32,
85 },
86
87 /// The `tlf2a` magic header was missing or malformed.
88 ///
89 /// `found` is the first up-to-32 bytes of the file (per spec Security
90 /// Posture — capped to prevent log spam from adversarial inputs).
91 /// Raised by [`crate::tlf::parse_tlf`] when the magic prefix mismatches
92 /// or when the numeric header fields are structurally invalid.
93 #[error("invalid tlf header: found {found:?}")]
94 InvalidTlfHeader {
95 /// Up to 32 bytes of observed header for diagnostic display.
96 found: Vec<u8>,
97 },
98
99 /// A `.tlf` file's glyph table failed to parse.
100 ///
101 /// `reason` is a short human description; `line` is the 1-indexed line
102 /// number at which the parser detected the problem. Distinct from
103 /// [`FigletError::FontParse`] because TLF carries different semantics
104 /// (UTF-8 multicolumn glyphs, multicolor cell markers) and downstream
105 /// callers may want to recover differently from each.
106 #[error("tlf parse error at line {line}: {reason}")]
107 TlfParse {
108 /// Short human-readable description of the parse failure.
109 reason: String,
110 /// 1-indexed line number where the parse error was detected.
111 line: u32,
112 },
113
114 /// A `-F <chain>` segment named a filter that is not in the supported
115 /// set (or whose leaf is disabled at compile-time).
116 ///
117 /// `name` is the offending segment as supplied; `available` enumerates
118 /// the valid filter names (in declaration order) so the CLI can emit a
119 /// helpful diagnostic per FR-016 and spec Edge Cases. Raised by
120 /// [`crate::filter::FilterChain::parse`].
121 #[error("unknown filter: {name}; available: {available:?}")]
122 UnknownFilter {
123 /// Offending filter name from the `-F` chain.
124 name: String,
125 /// Valid filter names in declaration order.
126 available: Vec<String>,
127 },
128
129 /// A CLI or library caller requested an export format whose leaf is
130 /// disabled at compile time (FR-016 + Phase 7 / T046).
131 ///
132 /// `requested` is the user-supplied format name (e.g. `"html"`);
133 /// `available` enumerates the format names whose leaves ARE enabled in
134 /// this build so the CLI can produce a helpful diagnostic. Raised by
135 /// [`crate::export::write_export`].
136 #[error("unsupported export format: {requested}; available: {available:?}")]
137 UnsupportedExportFormat {
138 /// Offending export format name as supplied by the caller.
139 requested: String,
140 /// Format names whose leaves ARE compiled into this build.
141 available: Vec<String>,
142 },
143
144 /// Strict-compat mode encountered input it cannot byte-equal-match
145 /// against the documented target (`toilet 0.3-1` or `figlet 2.2.5`).
146 ///
147 /// `mode` identifies which strict-compat target was active; `detail` is
148 /// a short human-readable description of the unmappable construct
149 /// (e.g., `"TLF multicolor glyph not representable in toilet 16-color floor"`).
150 ///
151 /// Raised by [`crate::strict_toilet::strict_render`] (gated by the
152 /// `toilet-strict-compat` leaf) and by future figlet-2.2.5 strict
153 /// invariants when no upstream byte-equal mapping exists for a given
154 /// input. The variant is feature-gated free — it is always compiled so
155 /// library callers can `match` on it regardless of which strict-compat
156 /// leaf is enabled at build time (per FR-016 + AD-005).
157 #[error("strict-compat violation ({mode:?}): {detail}")]
158 StrictCompatViolation {
159 /// Which strict-compat target was active when the violation occurred.
160 mode: StrictTarget,
161 /// Short description of the unmappable construct.
162 detail: String,
163 },
164
165 /// An internal invariant was violated. Indicates a bug in the library;
166 /// please file an issue.
167 #[error("internal error: {0}")]
168 Internal(&'static str),
169}
170
171/// Strict-compat target identifier carried by
172/// [`FigletError::StrictCompatViolation`] (E012 US6 — AD-005 + FR-016).
173///
174/// `Figlet225` is the existing `strict-compat` leaf (figlet 2.2.5 byte-equal
175/// argv parser + diagnostics). `Toilet031` is the Phase 8
176/// `toilet-strict-compat` leaf (toilet 0.3-1 byte-equal renderer + filter
177/// chain + 16-color floor).
178///
179/// Marked `#[non_exhaustive]` so future targets (e.g., a frozen figlet 2.2.4
180/// or a future toilet 0.4 line) remain non-breaking additions per AD-013.
181#[non_exhaustive]
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183pub enum StrictTarget {
184 /// Byte-equal compatibility with upstream `toilet 0.3-1`.
185 Toilet031,
186 /// Byte-equal compatibility with upstream `figlet 2.2.5`.
187 Figlet225,
188}