Skip to main content

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}