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}