Skip to main content

manifold/
error.rs

1// Error taxonomy for the Manifold library surface.
2//
3// One error type across all decoders (MNF5/6/7/8, refdecode) and encode
4// fallibility, replacing the per-module String returns (library hardening:
5// downstream consumers can match on the category, not parse messages).
6//
7// Design constraints from the repo's own rules:
8// - Deterministic-integer decoding contract unaffected (errors are about
9//   validation, not arithmetic).
10// - Panic-free untrusted-input handling: every malformed-input path maps to
11//   `Error::Format`, never a panic (audit/2026-10-25 + review/2026-10-25).
12
13use std::fmt;
14
15/// Every fallible library operation returns this.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Error {
18    /// The file/stream is structurally invalid for its claimed magic:
19    /// bad magic, impossible dims, tile_count mismatch, truncated records,
20    /// dangling/shape-mismatched references, invalid transform, reserved
21    /// bits/backend set, trailing bytes, decompressed-size violation.
22    /// `detail` is human-only (never parsed); `position` is the byte or tile
23    /// index where validation failed when known.
24    Format {
25        /// Human-readable description (never parsed).
26        detail: String,
27        /// Byte/tile index when known.
28        position: Option<u64>,
29    },
30    /// The stream is structurally plausible but claims more bytes/pixels/
31    /// tiles than the configured `DecodeLimits` allow.
32    Limits {
33        /// Which limit (e.g. `"max_pixels"`).
34        which: &'static str,
35        /// The configured ceiling.
36        limit: u64,
37        /// The rejected value.
38        actual: u64,
39    },
40    /// Decompression failed (corrupt zlib/zstd stream, bad checksum).
41    Decompress(&'static str),
42    /// Container/backend/feature not supported by this build
43    /// (reserved backend, feature-gated coder compiled out).
44    Unsupported {
45        /// `what` names the unshipped/reserved feature.
46        what: String,
47    },
48    /// I/O failure reading the input or writing the output PNG.
49    Io(String),
50    /// Image I/O (decode-to-RGB8 of a JPEG/PNG source, PNG encode).
51    ImageIo(String),
52    /// Internal invariant that the library treats as unreachable (should
53    /// never surface to callers; a bug if it does).
54    Internal(&'static str),
55}
56
57impl fmt::Display for Error {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Error::Format { detail, position } => match position {
61                Some(p) => write!(f, "invalid stream: {detail} (at {p})"),
62                None => write!(f, "invalid stream: {detail}"),
63            },
64            Error::Limits {
65                which,
66                limit,
67                actual,
68            } => {
69                write!(f, "{which} limit exceeded: {actual} > {limit}")
70            }
71            Error::Decompress(what) => write!(f, "decompress: {what}"),
72            Error::Unsupported { what } => write!(f, "unsupported: {what}"),
73            Error::Io(e) => write!(f, "i/o: {e}"),
74            Error::ImageIo(e) => write!(f, "image: {e}"),
75            Error::Internal(what) => write!(f, "internal error: {what}"),
76        }
77    }
78}
79
80impl std::error::Error for Error {}
81
82impl From<std::io::Error> for Error {
83    fn from(e: std::io::Error) -> Self {
84        Error::Io(e.to_string())
85    }
86}
87
88/// Shorthand alias for library results.
89pub type Result<T> = std::result::Result<T, Error>;
90
91// Internal constructors (keep call sites terse; the lib never exposes
92// constructing Format-detail strings as public API in v1.x).
93
94impl Error {
95    pub(crate) fn fmt_at(detail: impl fmt::Display, position: u64) -> Error {
96        Error::Format {
97            detail: detail.to_string(),
98            position: Some(position),
99        }
100    }
101
102    /// Public constructor: a format/descriptive error from an explicit string.
103    pub fn msg(detail: impl fmt::Display) -> Error {
104        Error::Format {
105            detail: detail.to_string(),
106            position: None,
107        }
108    }
109
110    pub(crate) fn fmt_msg(detail: impl fmt::Display) -> Error {
111        Error::msg(detail)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn display_is_stable() {
121        let e = Error::fmt_at("tile 3 truncated", 3);
122        assert_eq!(e.to_string(), "invalid stream: tile 3 truncated (at 3)");
123        let e = Error::Limits {
124            which: "pixels",
125            limit: 67_108_864,
126            actual: 67_108_865,
127        };
128        assert_eq!(e.to_string(), "pixels limit exceeded: 67108865 > 67108864");
129    }
130
131    #[test]
132    fn error_is_std_error() {
133        fn assert_error<T: std::error::Error>(_: &T) {}
134        assert_error(&Error::Decompress("x"));
135    }
136}