Skip to main content

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}