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/// Measured on the glTF WASM module, that is 2.6 KiB of gzipped code. It is not
92/// what dominates, and this comment says so because the first guess was wrong:
93/// the message text and the `format!` that builds it are worth 16 KiB in the
94/// same module, and no shape of the error type reaches those.
95///
96/// The kind is matched with [`kind`](Self::kind) rather than by pattern, and it
97/// is `#[non_exhaustive]`, so a later release can tell one refusal from another
98/// without a major bump.
99///
100/// ```
101/// use draco_core::{DracoError, ErrorKind};
102///
103/// let error = DracoError::unsupported_feature("multi-pass attribute coding");
104/// assert_eq!(error.kind(), ErrorKind::UnsupportedFeature);
105/// assert_eq!(error.message(), "multi-pass attribute coding");
106/// assert_eq!(
107/// error.to_string(),
108/// "Unsupported feature: multi-pass attribute coding"
109/// );
110/// ```
111#[derive(Clone, PartialEq, Eq)]
112pub struct DracoError {
113 inner: Box<Inner>,
114}
115
116impl DracoError {
117 /// Builds an error of `kind` carrying `message`.
118 ///
119 /// Cold and never inlined so that the allocation is emitted once rather
120 /// than at each of the several hundred sites that construct an error.
121 #[cold]
122 #[inline(never)]
123 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
124 Self {
125 inner: Box::new(Inner {
126 kind,
127 message: message.into(),
128 }),
129 }
130 }
131
132 /// What was refused. See [`ErrorKind`].
133 pub fn kind(&self) -> ErrorKind {
134 self.inner.kind
135 }
136
137 /// The message alone, without the fixed text [`ErrorKind::as_str`] adds.
138 ///
139 /// Empty for kinds that carry no detail.
140 pub fn message(&self) -> &str {
141 &self.inner.message
142 }
143
144 /// A generic failure. See [`ErrorKind::General`].
145 pub fn general(message: impl Into<String>) -> Self {
146 Self::new(ErrorKind::General, message)
147 }
148
149 /// A file or stream failure. See [`ErrorKind::Io`].
150 pub fn io(message: impl Into<String>) -> Self {
151 Self::new(ErrorKind::Io, message)
152 }
153
154 /// A rejected caller-provided parameter. See [`ErrorKind::InvalidParameter`].
155 pub fn invalid_parameter(message: impl Into<String>) -> Self {
156 Self::new(ErrorKind::InvalidParameter, message)
157 }
158
159 /// A known but unsupported bitstream version. See [`ErrorKind::UnsupportedVersion`].
160 pub fn unsupported_version(message: impl Into<String>) -> Self {
161 Self::new(ErrorKind::UnsupportedVersion, message)
162 }
163
164 /// An unidentifiable bitstream version. See [`ErrorKind::UnknownVersion`].
165 pub fn unknown_version(message: impl Into<String>) -> Self {
166 Self::new(ErrorKind::UnknownVersion, message)
167 }
168
169 /// A bitstream feature this crate does not implement. See [`ErrorKind::UnsupportedFeature`].
170 pub fn unsupported_feature(message: impl Into<String>) -> Self {
171 Self::new(ErrorKind::UnsupportedFeature, message)
172 }
173
174 /// A bitstream version outside the supported range.
175 /// See [`ErrorKind::BitstreamVersionUnsupported`].
176 pub fn bitstream_version_unsupported() -> Self {
177 Self::new(ErrorKind::BitstreamVersionUnsupported, String::new())
178 }
179
180 /// A failed buffer read or write. See [`ErrorKind::Buffer`].
181 pub fn buffer(message: impl Into<String>) -> Self {
182 Self::new(ErrorKind::Buffer, message)
183 }
184
185 /// A decode that asked for more memory than its input could describe.
186 /// See [`ErrorKind::AllocationExceedsInput`].
187 /// Prefixes this error's message with `context`, keeping its kind.
188 ///
189 /// The kind is what a caller matches on, so a layer that adds context has
190 /// to carry it through. Rebuilding the error as
191 /// [`general`](Self::general) instead flattens every refusal underneath
192 /// into one kind -- which is how a caller's own
193 /// [`LimitExceeded`](ErrorKind::LimitExceeded) ceiling became
194 /// indistinguishable from a corrupt file.
195 #[cold]
196 #[inline(never)]
197 #[must_use]
198 pub fn context(self, context: impl std::fmt::Display) -> Self {
199 let kind = self.inner.kind;
200 let message = if self.inner.message.is_empty() {
201 format!("{context}")
202 } else {
203 format!("{context}: {}", self.inner.message)
204 };
205 Self::new(kind, message)
206 }
207
208 pub fn allocation_exceeds_input(requested_bytes: usize, stream_bytes: usize) -> Self {
209 Self::new(
210 ErrorKind::AllocationExceedsInput,
211 format!("would allocate {requested_bytes} bytes from a {stream_bytes} byte stream"),
212 )
213 }
214}
215
216impl fmt::Display for DracoError {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 if self.inner.message.is_empty() {
219 f.write_str(self.inner.kind.as_str())
220 } else {
221 write!(f, "{}: {}", self.inner.kind.as_str(), self.inner.message)
222 }
223 }
224}
225
226/// Prints the kind and the message rather than the box, so that a `{:?}` of a
227/// `Result` reads the way the enum's did.
228impl fmt::Debug for DracoError {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 f.debug_struct("DracoError")
231 .field("kind", &self.inner.kind)
232 .field("message", &self.inner.message)
233 .finish()
234 }
235}
236
237impl std::error::Error for DracoError {}
238
239/// Convenience result type for operations that only report success or failure.
240pub type Status = Result<(), DracoError>;
241
242impl From<()> for DracoError {
243 fn from(_: ()) -> Self {
244 DracoError::general("Unknown error")
245 }
246}
247
248/// Returns a successful [`Status`].
249pub fn ok_status() -> Status {
250 Ok(())
251}
252
253/// Creates a generic [`DracoError`] from a message.
254pub fn error_status(msg: impl Into<String>) -> DracoError {
255 DracoError::general(msg)
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 /// The reason the type is shaped this way: every fallible function in the
263 /// crate returns `Status`, so the size of the failure case is paid by the
264 /// success case at every call site.
265 #[test]
266 fn a_status_is_a_pointer_wide_and_the_success_case_needs_no_drop() {
267 assert_eq!(
268 std::mem::size_of::<Status>(),
269 std::mem::size_of::<*const ()>()
270 );
271 assert_eq!(
272 std::mem::size_of::<DracoError>(),
273 std::mem::size_of::<*const ()>()
274 );
275 // `Ok(())` must be the null pointer, or the niche is not being used and
276 // the discriminant costs another word.
277 assert_eq!(
278 std::mem::size_of::<Option<Status>>(),
279 2 * std::mem::size_of::<usize>()
280 );
281 }
282
283 #[test]
284 fn a_kind_without_a_message_displays_as_the_kind_alone() {
285 assert_eq!(
286 DracoError::bitstream_version_unsupported().to_string(),
287 "Bitstream version unsupported"
288 );
289 assert_eq!(DracoError::bitstream_version_unsupported().message(), "");
290 }
291
292 #[test]
293 fn the_kind_survives_a_round_trip_through_display() {
294 let error = DracoError::buffer("read past end");
295 assert_eq!(error.kind(), ErrorKind::Buffer);
296 assert_eq!(error.to_string(), "Buffer decode error: read past end");
297 }
298
299 #[test]
300 fn two_errors_of_different_kinds_are_not_equal() {
301 assert_ne!(DracoError::general("x"), DracoError::io("x"));
302 assert_eq!(DracoError::general("x"), DracoError::general("x"));
303 }
304}