Skip to main content

oxideav_core/
error.rs

1//! Shared error type for oxideav.
2//!
3//! # Taxonomy
4//!
5//! Pick the variant by what the *caller* should do about it:
6//!
7//! * [`Error::InvalidData`] — the input violates its format's rules;
8//!   retrying or feeding more bytes won't help. Skip the packet / abort
9//!   the stream.
10//! * [`Error::Unsupported`] — the input is (as far as we can tell)
11//!   valid, but exercises a feature this implementation doesn't cover.
12//!   A different implementation might succeed.
13//! * [`Error::Eof`] — the logical end of the stream was reached. Not a
14//!   failure when it happens between packets; drain and stop.
15//! * [`Error::NeedMore`] — a push-style parser stopped mid-unit; feed
16//!   more bytes and call again. Unlike `Eof`, progress resumes.
17//! * [`Error::FormatNotFound`] / [`Error::CodecNotFound`] — registry
18//!   probe/lookup misses.
19//! * [`Error::ResourceExhausted`] — a configured cap or pool limit
20//!   fired; hard-reject the input or back off, never retry blindly.
21//! * [`Error::Io`] / [`Error::Other`] — transport problems and
22//!   everything else.
23
24use thiserror::Error;
25
26/// Convenience alias: `std::result::Result` pinned to [`enum@Error`].
27pub type Result<T> = std::result::Result<T, Error>;
28
29/// The error type shared by every oxideav crate. See the
30/// [module docs](self) for which variant to pick.
31#[derive(Debug, Error)]
32pub enum Error {
33    /// An underlying transport / filesystem operation failed.
34    #[error("I/O error: {0}")]
35    Io(#[from] std::io::Error),
36
37    /// Valid input exercising a feature this implementation lacks.
38    #[error("unsupported: {0}")]
39    Unsupported(String),
40
41    /// The input violates its format's rules; not retryable.
42    #[error("invalid data: {0}")]
43    InvalidData(String),
44
45    /// Logical end of stream (clean between packets; short otherwise).
46    #[error("end of stream")]
47    Eof,
48
49    /// Push-parser starvation: feed more bytes and call again.
50    #[error("need more data")]
51    NeedMore,
52
53    /// No registered container format matched the probe subject.
54    #[error("format not found: {0}")]
55    FormatNotFound(String),
56
57    /// No registered codec matched the requested name or tag.
58    #[error("codec not found: {0}")]
59    CodecNotFound(String),
60
61    /// A decoder (or arena pool) refused to allocate or proceed because
62    /// doing so would exceed a configured [`DecoderLimits`](crate::DecoderLimits)
63    /// cap, or because a pool has no free slot. This is the canonical
64    /// "DoS protection fired" error — callers should treat it as a hard
65    /// rejection of the input or a transient backpressure signal, never
66    /// retry blindly.
67    #[error("resource exhausted: {0}")]
68    ResourceExhausted(String),
69
70    /// Anything that doesn't fit the other variants; the message
71    /// carries the whole story.
72    #[error("{0}")]
73    Other(String),
74}
75
76impl Error {
77    /// Construct an [`Error::Unsupported`] with the given message.
78    pub fn unsupported(msg: impl Into<String>) -> Self {
79        Self::Unsupported(msg.into())
80    }
81
82    /// Construct an [`Error::InvalidData`] with the given message.
83    pub fn invalid(msg: impl Into<String>) -> Self {
84        Self::InvalidData(msg.into())
85    }
86
87    /// Construct an [`Error::Other`] with the given message.
88    pub fn other(msg: impl Into<String>) -> Self {
89        Self::Other(msg.into())
90    }
91
92    /// Construct a [`Error::ResourceExhausted`] with the given message.
93    /// Use this from any decoder that has just hit a `DecoderLimits` cap
94    /// or an arena-pool exhaustion.
95    pub fn resource_exhausted(msg: impl Into<String>) -> Self {
96        Self::ResourceExhausted(msg.into())
97    }
98
99    /// Construct a [`Error::FormatNotFound`] with the given probe
100    /// subject (file name, extension, or magic description).
101    pub fn format_not_found(msg: impl Into<String>) -> Self {
102        Self::FormatNotFound(msg.into())
103    }
104
105    /// Construct a [`Error::CodecNotFound`] with the codec name or tag
106    /// that missed the registry.
107    pub fn codec_not_found(msg: impl Into<String>) -> Self {
108        Self::CodecNotFound(msg.into())
109    }
110
111    /// `true` for [`Error::Eof`]. `Error` cannot implement `PartialEq`
112    /// (the `Io` variant wraps `std::io::Error`), so drain loops that
113    /// need "stop cleanly on end-of-stream" branch on this instead of
114    /// a `matches!` at every call site.
115    pub fn is_eof(&self) -> bool {
116        matches!(self, Self::Eof)
117    }
118
119    /// `true` for [`Error::NeedMore`] — the push-parser "feed me more
120    /// bytes and retry" signal.
121    pub fn is_need_more(&self) -> bool {
122        matches!(self, Self::NeedMore)
123    }
124
125    /// `true` for [`Error::ResourceExhausted`] — the "DoS cap fired"
126    /// signal that must not be blindly retried.
127    pub fn is_resource_exhausted(&self) -> bool {
128        matches!(self, Self::ResourceExhausted(_))
129    }
130
131    /// `true` when the error only says the stream stopped short —
132    /// [`Error::Eof`] or [`Error::NeedMore`] — rather than reporting
133    /// malformed or unsupported content. Useful for probe loops that
134    /// try successive parsers on a growing prefix: starvation means
135    /// "inconclusive, buffer more", anything else means "this parser
136    /// has a verdict".
137    pub fn is_starved(&self) -> bool {
138        matches!(self, Self::Eof | Self::NeedMore)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn constructors_produce_matching_variants() {
148        assert!(matches!(
149            Error::format_not_found("mkv"),
150            Error::FormatNotFound(s) if s == "mkv"
151        ));
152        assert!(matches!(
153            Error::codec_not_found("vp8"),
154            Error::CodecNotFound(s) if s == "vp8"
155        ));
156        assert!(matches!(
157            Error::resource_exhausted("pool"),
158            Error::ResourceExhausted(s) if s == "pool"
159        ));
160    }
161
162    #[test]
163    fn predicates_partition_correctly() {
164        assert!(Error::Eof.is_eof());
165        assert!(!Error::Eof.is_need_more());
166        assert!(Error::NeedMore.is_need_more());
167        assert!(!Error::NeedMore.is_eof());
168        assert!(Error::Eof.is_starved());
169        assert!(Error::NeedMore.is_starved());
170        assert!(Error::resource_exhausted("x").is_resource_exhausted());
171        for e in [
172            Error::invalid("bad"),
173            Error::unsupported("feature"),
174            Error::other("misc"),
175            Error::format_not_found("f"),
176            Error::codec_not_found("c"),
177        ] {
178            assert!(!e.is_eof());
179            assert!(!e.is_need_more());
180            assert!(!e.is_starved());
181            assert!(!e.is_resource_exhausted());
182        }
183    }
184
185    #[test]
186    fn display_messages_are_stable() {
187        assert_eq!(Error::Eof.to_string(), "end of stream");
188        assert_eq!(Error::NeedMore.to_string(), "need more data");
189        assert_eq!(Error::invalid("x").to_string(), "invalid data: x");
190        assert_eq!(Error::unsupported("y").to_string(), "unsupported: y");
191        assert_eq!(
192            Error::format_not_found("z").to_string(),
193            "format not found: z"
194        );
195        assert_eq!(
196            Error::codec_not_found("w").to_string(),
197            "codec not found: w"
198        );
199        assert_eq!(
200            Error::resource_exhausted("v").to_string(),
201            "resource exhausted: v"
202        );
203        assert_eq!(Error::other("u").to_string(), "u");
204    }
205}