draco_core/status.rs
1use std::fmt;
2
3/// What a [`DracoError`] refused, as a value that can be compared and copied.
4///
5/// Separate from the error itself so that the error can stay one pointer wide:
6/// the kind and its message live together behind a single allocation, and
7/// `Result<(), DracoError>` is a pointer-sized value returned in a register.
8/// See [`DracoError`] for why that matters.
9///
10/// Non-exhaustive: a decoder that learns to tell one refusal from another
11/// should be able to say so without a major release. Match with a `_` arm.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14pub enum ErrorKind {
15 /// Generic error.
16 ///
17 /// Mirrors upstream's `Status::DRACO_ERROR`, whose own comment reads "used
18 /// for general errors".
19 General,
20 /// File or stream I/O error.
21 Io,
22 /// Invalid caller-provided parameter.
23 InvalidParameter,
24 /// Bitstream version is known but unsupported.
25 UnsupportedVersion,
26 /// Bitstream version could not be identified.
27 UnknownVersion,
28 /// Bitstream uses a feature this crate does not support.
29 UnsupportedFeature,
30 /// Bitstream version is outside the supported range.
31 BitstreamVersionUnsupported,
32 /// Buffer read or write failed.
33 Buffer,
34 /// A decode would allocate more than the stream it is reading could
35 /// plausibly describe.
36 ///
37 /// The bound is a ratio against the input size, not a cap on geometry — see
38 /// `decode_budget`. A stream that trips it is malformed or adversarial; a
39 /// large but genuine mesh scales its own budget with it.
40 AllocationExceedsInput,
41 /// A decode would produce more than the caller's [`DecodeLimits`](crate::DecodeLimits) allow.
42 ///
43 /// Distinct from [`AllocationExceedsInput`](Self::AllocationExceedsInput)
44 /// on purpose: that one says the file is malformed, this one says the file
45 /// may well be fine and the caller declined to decode something that
46 /// large. Raise the limits, or use
47 /// [`DecodeLimits::permissive`](crate::DecodeLimits::permissive).
48 LimitExceeded,
49}
50
51impl ErrorKind {
52 /// The fixed text this kind contributes to [`DracoError`]'s `Display`.
53 pub fn as_str(self) -> &'static str {
54 match self {
55 ErrorKind::General => "General error",
56 ErrorKind::Io => "IO error",
57 ErrorKind::InvalidParameter => "Invalid parameter",
58 ErrorKind::UnsupportedVersion => "Unsupported version",
59 ErrorKind::UnknownVersion => "Unknown version",
60 ErrorKind::UnsupportedFeature => "Unsupported feature",
61 ErrorKind::BitstreamVersionUnsupported => "Bitstream version unsupported",
62 ErrorKind::Buffer => "Buffer decode error",
63 ErrorKind::AllocationExceedsInput => "Allocation exceeds input",
64 ErrorKind::LimitExceeded => "Decode limit exceeded",
65 }
66 }
67}
68
69impl fmt::Display for ErrorKind {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str(self.as_str())
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76struct Inner {
77 kind: ErrorKind,
78 message: String,
79}
80
81/// Error returned by Draco decoding, encoding, and data model operations.
82///
83/// One pointer wide, in the shape of [`std::io::Error`]: the [`ErrorKind`] and
84/// the message live behind a single boxed allocation that is only made on the
85/// failure path. This is not a style preference. Almost every decode and encode
86/// function in this crate returns [`Status`], and when the error was an enum
87/// carrying a `String` inline, `Result<(), DracoError>` was 32 bytes and needed
88/// dropping: every one of those functions returned through a hidden out-pointer
89/// and every `?` expanded to `String` drop glue. Boxing makes the success case a
90/// null pointer in a register and reduces the drop glue to one shared function.
91/// That is not where most of the error path's code size goes: the message text
92/// and the `format!` that builds it weigh several times more, and no shape of
93/// the error type reaches those.
94///
95/// The kind is matched with [`kind`](Self::kind) rather than by pattern, and it
96/// is `#[non_exhaustive]`, so a later release can tell one refusal from another
97/// without a major bump.
98///
99/// ```
100/// use draco_core::{DracoError, ErrorKind};
101///
102/// let error = DracoError::unsupported_feature("multi-pass attribute coding");
103/// assert_eq!(error.kind(), ErrorKind::UnsupportedFeature);
104/// assert_eq!(error.message(), "multi-pass attribute coding");
105/// assert_eq!(
106/// error.to_string(),
107/// "Unsupported feature: multi-pass attribute coding"
108/// );
109/// ```
110#[derive(Clone, PartialEq, Eq)]
111pub struct DracoError {
112 inner: Box<Inner>,
113}
114
115impl DracoError {
116 /// Builds an error of `kind` carrying `message`.
117 ///
118 /// Cold and never inlined so that the allocation is emitted once rather
119 /// than at each of the several hundred sites that construct an error.
120 #[cold]
121 #[inline(never)]
122 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
123 Self {
124 inner: Box::new(Inner {
125 kind,
126 message: message.into(),
127 }),
128 }
129 }
130
131 /// What was refused. See [`ErrorKind`].
132 pub fn kind(&self) -> ErrorKind {
133 self.inner.kind
134 }
135
136 /// The message alone, without the fixed text [`ErrorKind::as_str`] adds.
137 ///
138 /// Empty for kinds that carry no detail.
139 pub fn message(&self) -> &str {
140 &self.inner.message
141 }
142
143 /// A generic failure. See [`ErrorKind::General`].
144 pub fn general(message: impl Into<String>) -> Self {
145 Self::new(ErrorKind::General, message)
146 }
147
148 /// A file or stream failure. See [`ErrorKind::Io`].
149 pub fn io(message: impl Into<String>) -> Self {
150 Self::new(ErrorKind::Io, message)
151 }
152
153 /// A rejected caller-provided parameter. See [`ErrorKind::InvalidParameter`].
154 pub fn invalid_parameter(message: impl Into<String>) -> Self {
155 Self::new(ErrorKind::InvalidParameter, message)
156 }
157
158 /// A known but unsupported bitstream version. See [`ErrorKind::UnsupportedVersion`].
159 pub fn unsupported_version(message: impl Into<String>) -> Self {
160 Self::new(ErrorKind::UnsupportedVersion, message)
161 }
162
163 /// An unidentifiable bitstream version. See [`ErrorKind::UnknownVersion`].
164 pub fn unknown_version(message: impl Into<String>) -> Self {
165 Self::new(ErrorKind::UnknownVersion, message)
166 }
167
168 /// A bitstream feature this crate does not implement. See [`ErrorKind::UnsupportedFeature`].
169 pub fn unsupported_feature(message: impl Into<String>) -> Self {
170 Self::new(ErrorKind::UnsupportedFeature, message)
171 }
172
173 /// A bitstream version outside the supported range.
174 /// See [`ErrorKind::BitstreamVersionUnsupported`].
175 pub fn bitstream_version_unsupported() -> Self {
176 Self::new(ErrorKind::BitstreamVersionUnsupported, String::new())
177 }
178
179 /// A failed buffer read or write. See [`ErrorKind::Buffer`].
180 pub fn buffer(message: impl Into<String>) -> Self {
181 Self::new(ErrorKind::Buffer, message)
182 }
183
184 /// A decode that asked for more memory than its input could describe.
185 /// See [`ErrorKind::AllocationExceedsInput`].
186 /// Prefixes this error's message with `context`, keeping its kind.
187 ///
188 /// The kind is what a caller matches on, so a layer that adds context has
189 /// to carry it through. Rebuilding the error as
190 /// [`general`](Self::general) instead flattens every refusal underneath
191 /// into one kind -- which is how a caller's own
192 /// [`LimitExceeded`](ErrorKind::LimitExceeded) ceiling became
193 /// indistinguishable from a corrupt file.
194 #[cold]
195 #[inline(never)]
196 #[must_use]
197 pub fn context(self, context: impl std::fmt::Display) -> Self {
198 let kind = self.inner.kind;
199 let message = if self.inner.message.is_empty() {
200 format!("{context}")
201 } else {
202 format!("{context}: {}", self.inner.message)
203 };
204 Self::new(kind, message)
205 }
206
207 pub fn allocation_exceeds_input(requested_bytes: usize, stream_bytes: usize) -> Self {
208 Self::new(
209 ErrorKind::AllocationExceedsInput,
210 format!("would allocate {requested_bytes} bytes from a {stream_bytes} byte stream"),
211 )
212 }
213}
214
215impl fmt::Display for DracoError {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 if self.inner.message.is_empty() {
218 f.write_str(self.inner.kind.as_str())
219 } else {
220 write!(f, "{}: {}", self.inner.kind.as_str(), self.inner.message)
221 }
222 }
223}
224
225/// Prints the kind and the message rather than the box, so that a `{:?}` of a
226/// `Result` reads the way the enum's did.
227impl fmt::Debug for DracoError {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 f.debug_struct("DracoError")
230 .field("kind", &self.inner.kind)
231 .field("message", &self.inner.message)
232 .finish()
233 }
234}
235
236impl std::error::Error for DracoError {}
237
238/// Convenience result type for operations that only report success or failure.
239pub type Status = Result<(), DracoError>;
240
241impl From<()> for DracoError {
242 fn from(_: ()) -> Self {
243 DracoError::general("Unknown error")
244 }
245}
246
247/// Returns a successful [`Status`].
248pub fn ok_status() -> Status {
249 Ok(())
250}
251
252/// Creates a generic [`DracoError`] from a message.
253pub fn error_status(msg: impl Into<String>) -> DracoError {
254 DracoError::general(msg)
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 /// The reason the type is shaped this way: every fallible function in the
262 /// crate returns `Status`, so the size of the failure case is paid by the
263 /// success case at every call site.
264 #[test]
265 fn a_status_is_a_pointer_wide_and_the_success_case_needs_no_drop() {
266 assert_eq!(
267 std::mem::size_of::<Status>(),
268 std::mem::size_of::<*const ()>()
269 );
270 assert_eq!(
271 std::mem::size_of::<DracoError>(),
272 std::mem::size_of::<*const ()>()
273 );
274 // `Ok(())` must be the null pointer, or the niche is not being used and
275 // the discriminant costs another word.
276 assert_eq!(
277 std::mem::size_of::<Option<Status>>(),
278 2 * std::mem::size_of::<usize>()
279 );
280 }
281
282 #[test]
283 fn a_kind_without_a_message_displays_as_the_kind_alone() {
284 assert_eq!(
285 DracoError::bitstream_version_unsupported().to_string(),
286 "Bitstream version unsupported"
287 );
288 assert_eq!(DracoError::bitstream_version_unsupported().message(), "");
289 }
290
291 #[test]
292 fn the_kind_survives_a_round_trip_through_display() {
293 let error = DracoError::buffer("read past end");
294 assert_eq!(error.kind(), ErrorKind::Buffer);
295 assert_eq!(error.to_string(), "Buffer decode error: read past end");
296 }
297
298 #[test]
299 fn two_errors_of_different_kinds_are_not_equal() {
300 assert_ne!(DracoError::general("x"), DracoError::io("x"));
301 assert_eq!(DracoError::general("x"), DracoError::general("x"));
302 }
303}