Skip to main content

otf_pixels_core/
error.rs

1//! The engine's error type.
2//!
3//! Errors are values end to end (ARCHITECTURE §Failure model). Malformed input
4//! is a [`PixelsError::Malformed`], never a panic — codec parsers must return
5//! this variant for every byte sequence a hostile source can produce.
6//!
7//! [`ErrorCode`] is the stable, machine-readable projection of an error and is
8//! part of the public API under semver (SPEC §Guarantees 4). The
9//! [`PixelsError`] variants carry human-readable detail and are
10//! `#[non_exhaustive]`; match on [`PixelsError::code`] when you need
11//! exhaustive, forward-compatible handling.
12
13use core::fmt;
14
15/// The engine's result alias.
16pub type Result<T, E = PixelsError> = core::result::Result<T, E>;
17
18/// A stable, machine-readable error classification.
19///
20/// Codes are part of the public API and follow semver: a code is never
21/// removed or repurposed, and host bindings may expose them directly. New
22/// codes may be added in minor releases, hence `#[non_exhaustive]`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24#[non_exhaustive]
25pub enum ErrorCode {
26    /// Reading from a source or writing to a sink failed.
27    Io,
28    /// The input bytes are not valid for the format they claim to be.
29    Malformed,
30    /// The format or feature is recognized but not implemented.
31    Unsupported,
32    /// A configured safety limit was exceeded (SPEC §Safety).
33    LimitExceeded,
34    /// A caller-supplied argument is invalid (e.g. a zero-width crop).
35    InvalidArgument,
36    /// The op graph is not evaluable as constructed.
37    Graph,
38}
39
40impl ErrorCode {
41    /// A short, stable, lowercase identifier for this code.
42    ///
43    /// Suitable for logs and for host bindings that surface string codes.
44    #[must_use]
45    pub const fn as_str(self) -> &'static str {
46        match self {
47            Self::Io => "io",
48            Self::Malformed => "malformed",
49            Self::Unsupported => "unsupported",
50            Self::LimitExceeded => "limit_exceeded",
51            Self::InvalidArgument => "invalid_argument",
52            Self::Graph => "graph",
53        }
54    }
55}
56
57impl fmt::Display for ErrorCode {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.write_str(self.as_str())
60    }
61}
62
63/// The safety limit that a [`PixelsError::LimitExceeded`] refers to.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[non_exhaustive]
66pub enum Limit {
67    /// Total pixel count (width × height) exceeded [`Limits::max_pixels`].
68    ///
69    /// [`Limits::max_pixels`]: crate::Limits::max_pixels
70    MaxPixels,
71    /// A single dimension exceeded the representable maximum.
72    Dimension,
73}
74
75impl Limit {
76    /// A short, stable identifier for this limit.
77    #[must_use]
78    pub const fn as_str(self) -> &'static str {
79        match self {
80            Self::MaxPixels => "max_pixels",
81            Self::Dimension => "dimension",
82        }
83    }
84}
85
86impl fmt::Display for Limit {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.write_str(self.as_str())
89    }
90}
91
92/// The error type returned by every fallible engine operation.
93///
94/// Construct these with the associated helpers ([`PixelsError::malformed`],
95/// [`PixelsError::unsupported`], …) rather than the variants directly, so that
96/// added fields stay non-breaking.
97#[derive(Debug)]
98#[non_exhaustive]
99pub enum PixelsError {
100    /// A source read or sink write failed.
101    Io {
102        /// What the engine was doing when the I/O failed.
103        context: &'static str,
104        /// The underlying operating system error.
105        source: std::io::Error,
106    },
107    /// Input bytes are invalid for their format.
108    ///
109    /// Every codec parser returns this instead of panicking, for any input.
110    Malformed {
111        /// The format being parsed, e.g. `"raw"`, `"png"`.
112        format: &'static str,
113        /// What was wrong, in human-readable terms.
114        detail: String,
115    },
116    /// A recognized format or feature that is not implemented.
117    Unsupported {
118        /// What is unsupported, in human-readable terms.
119        detail: String,
120    },
121    /// A safety limit was exceeded before any pixel allocation.
122    LimitExceeded {
123        /// Which limit was hit.
124        limit: Limit,
125        /// The value the input asked for.
126        requested: u64,
127        /// The configured maximum.
128        allowed: u64,
129    },
130    /// A caller-supplied argument is invalid.
131    InvalidArgument {
132        /// The parameter at fault, e.g. `"width"`.
133        parameter: &'static str,
134        /// Why it is invalid.
135        detail: String,
136    },
137    /// The op graph cannot be evaluated as constructed.
138    Graph {
139        /// Why the graph is invalid.
140        detail: String,
141    },
142}
143
144impl PixelsError {
145    /// The stable classification of this error.
146    #[must_use]
147    pub const fn code(&self) -> ErrorCode {
148        match self {
149            Self::Io { .. } => ErrorCode::Io,
150            Self::Malformed { .. } => ErrorCode::Malformed,
151            Self::Unsupported { .. } => ErrorCode::Unsupported,
152            Self::LimitExceeded { .. } => ErrorCode::LimitExceeded,
153            Self::InvalidArgument { .. } => ErrorCode::InvalidArgument,
154            Self::Graph { .. } => ErrorCode::Graph,
155        }
156    }
157
158    /// Wrap an I/O error with the engine context it happened in.
159    #[must_use]
160    pub fn io(context: &'static str, source: std::io::Error) -> Self {
161        Self::Io { context, source }
162    }
163
164    /// Report input bytes that are invalid for `format`.
165    #[must_use]
166    pub fn malformed(format: &'static str, detail: impl Into<String>) -> Self {
167        Self::Malformed {
168            format,
169            detail: detail.into(),
170        }
171    }
172
173    /// Report a recognized but unimplemented format or feature.
174    #[must_use]
175    pub fn unsupported(detail: impl Into<String>) -> Self {
176        Self::Unsupported {
177            detail: detail.into(),
178        }
179    }
180
181    /// Report an invalid caller-supplied argument.
182    #[must_use]
183    pub fn invalid_argument(parameter: &'static str, detail: impl Into<String>) -> Self {
184        Self::InvalidArgument {
185            parameter,
186            detail: detail.into(),
187        }
188    }
189
190    /// Report a graph that cannot be evaluated.
191    #[must_use]
192    pub fn graph(detail: impl Into<String>) -> Self {
193        Self::Graph {
194            detail: detail.into(),
195        }
196    }
197
198    /// Report an exceeded safety limit.
199    #[must_use]
200    pub const fn limit_exceeded(limit: Limit, requested: u64, allowed: u64) -> Self {
201        Self::LimitExceeded {
202            limit,
203            requested,
204            allowed,
205        }
206    }
207}
208
209impl fmt::Display for PixelsError {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Self::Io { context, source } => write!(f, "i/o error while {context}: {source}"),
213            Self::Malformed { format, detail } => {
214                write!(f, "malformed {format} input: {detail}")
215            }
216            Self::Unsupported { detail } => write!(f, "unsupported: {detail}"),
217            Self::LimitExceeded {
218                limit,
219                requested,
220                allowed,
221            } => write!(
222                f,
223                "limit `{limit}` exceeded: requested {requested}, allowed {allowed}"
224            ),
225            Self::InvalidArgument { parameter, detail } => {
226                write!(f, "invalid argument `{parameter}`: {detail}")
227            }
228            Self::Graph { detail } => write!(f, "invalid graph: {detail}"),
229        }
230    }
231}
232
233impl std::error::Error for PixelsError {
234    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
235        match self {
236            Self::Io { source, .. } => Some(source),
237            _ => None,
238        }
239    }
240}
241
242#[cfg(test)]
243#[allow(
244    clippy::unwrap_used,
245    clippy::indexing_slicing,
246    reason = "tests operate on known-good values and assert shapes directly"
247)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn code_is_stable_per_variant() {
253        assert_eq!(
254            PixelsError::malformed("raw", "x").code(),
255            ErrorCode::Malformed
256        );
257        assert_eq!(PixelsError::unsupported("x").code(), ErrorCode::Unsupported);
258        assert_eq!(PixelsError::graph("x").code(), ErrorCode::Graph);
259        assert_eq!(
260            PixelsError::invalid_argument("w", "x").code(),
261            ErrorCode::InvalidArgument
262        );
263        assert_eq!(
264            PixelsError::limit_exceeded(Limit::MaxPixels, 5, 4).code(),
265            ErrorCode::LimitExceeded
266        );
267        let io = PixelsError::io("reading", std::io::Error::other("boom"));
268        assert_eq!(io.code(), ErrorCode::Io);
269    }
270
271    #[test]
272    fn code_strings_are_stable() {
273        assert_eq!(ErrorCode::Io.as_str(), "io");
274        assert_eq!(ErrorCode::Malformed.as_str(), "malformed");
275        assert_eq!(ErrorCode::Unsupported.as_str(), "unsupported");
276        assert_eq!(ErrorCode::LimitExceeded.as_str(), "limit_exceeded");
277        assert_eq!(ErrorCode::InvalidArgument.as_str(), "invalid_argument");
278        assert_eq!(ErrorCode::Graph.as_str(), "graph");
279        assert_eq!(Limit::MaxPixels.as_str(), "max_pixels");
280        assert_eq!(Limit::Dimension.as_str(), "dimension");
281    }
282
283    #[test]
284    fn io_errors_expose_their_source() {
285        use std::error::Error as _;
286        let err = PixelsError::io("reading header", std::io::Error::other("boom"));
287        assert!(err.source().is_some());
288        assert!(PixelsError::graph("x").source().is_none());
289        assert!(err.to_string().contains("reading header"));
290    }
291
292    #[test]
293    fn limit_display_names_the_limit() {
294        let err = PixelsError::limit_exceeded(Limit::MaxPixels, 300, 268);
295        let text = err.to_string();
296        assert!(text.contains("max_pixels"), "{text}");
297        assert!(text.contains("300"), "{text}");
298        assert!(text.contains("268"), "{text}");
299    }
300}