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/// 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}