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}