mrc/error.rs
1//! Error types for MRC I/O and validation.
2//!
3//! The [`Error`] enum is the single error type returned by all fallible
4//! operations in this crate. It covers I/O failures, header issues, type
5//! mismatches, bounds errors, compression problems, and statistics mismatches.
6//!
7//! [`HeaderValidationError`] provides fine-grained diagnostics for header
8//! problems — dimensions, axis mapping, space group, labels, NVERSION, etc.
9//!
10//! # Example — matching specific errors
11//!
12//! ```rust
13//! use mrc::Error;
14//!
15//! fn check(err: &Error) -> &'static str {
16//! match err {
17//! Error::Io(_) => "I/O problem",
18//! Error::InvalidHeader => "bad header",
19//! Error::ModeMismatch { .. } => "wrong voxel type for this file",
20//! Error::BoundsError { .. } => "access outside volume",
21//! Error::FileSizeMismatch { .. } => "file truncated or has trailing data",
22//! _ => "other",
23//! }
24//! }
25//! assert_eq!(check(&Error::BoundsError { offset: None, shape: None, volume: None }), "access outside volume");
26//! ```
27
28#[cfg(feature = "serde")]
29use serde::{Deserialize, Serialize};
30
31/// The top-level error type for MRC I/O operations.
32///
33/// Most fallible functions in this crate return `Result<T, Error>`.
34///
35/// This enum is `#[non_exhaustive]` — new variants may be added in minor
36/// releases without breaking exhaustive matches. Use a wildcard arm (`_`)
37/// when matching all variants.
38///
39/// # Example — constructing and displaying an error
40///
41/// ```rust
42/// use mrc::Error;
43///
44/// let err = Error::InvalidHeader;
45/// assert_eq!(err.to_string(), "Invalid MRC header");
46/// ```
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48#[derive(thiserror::Error, Debug)]
49#[non_exhaustive]
50pub enum Error {
51 /// An underlying I/O operation failed.
52 #[error("IO error: {0}")]
53 #[cfg_attr(feature = "serde", serde(skip))]
54 Io(#[from] std::io::Error),
55 /// The MRC header is malformed or fails basic validation.
56 #[error("Invalid MRC header")]
57 InvalidHeader,
58 /// The MRC mode value is not supported by this crate.
59 #[error("Unsupported mode")]
60 UnsupportedMode,
61 /// A requested read or write falls outside the volume bounds.
62 ///
63 /// The optional fields provide context about which block was requested
64 /// and what the volume dimensions were.
65 #[error(
66 "Bounds error at offset ({ox},{oy},{oz}) shape ({sx},{sy},{sz}) of volume ({vx},{vy},{vz})",
67 ox = .offset.map_or(0, |o| o[0]),
68 oy = .offset.map_or(0, |o| o[1]),
69 oz = .offset.map_or(0, |o| o[2]),
70 sx = .shape.map_or(0, |s| s[0]),
71 sy = .shape.map_or(0, |s| s[1]),
72 sz = .shape.map_or(0, |s| s[2]),
73 vx = .volume.map_or(0, |v| v[0]),
74 vy = .volume.map_or(0, |v| v[1]),
75 vz = .volume.map_or(0, |v| v[2]),
76 )]
77 BoundsError {
78 /// Offset of the requested block `[x, y, z]`.
79 offset: Option<[usize; 3]>,
80 /// Shape of the requested block `[sx, sy, sz]`.
81 shape: Option<[usize; 3]>,
82 /// Dimensions of the volume `[nx, ny, nz]`.
83 volume: Option<[usize; 3]>,
84 },
85 /// The voxel type does not match the file's mode.
86 #[error("Type mismatch: expected {expected} bytes per voxel, got {actual} bytes")]
87 TypeMismatch {
88 /// Number of bytes per voxel expected by the decoder.
89 expected: usize,
90 /// Number of bytes provided.
91 actual: usize,
92 },
93 /// The data vector length does not match the declared block shape.
94 #[error("Invalid block shape: expected {expected} elements, got {actual}")]
95 BlockShapeMismatch {
96 /// Number of elements expected from the shape.
97 expected: usize,
98 /// Number of elements actually provided.
99 actual: usize,
100 },
101
102 /// The requested voxel type does not match the file's stored mode.
103 #[error("Mode mismatch: file stores {file_mode:?}, requested {requested_mode:?}{}",
104 match .offset {
105 Some(o) => format!(" at offset ({},{},{})", o[0], o[1], o[2]),
106 None => String::new(),
107 }
108 )]
109 ModeMismatch {
110 /// The mode stored in the file.
111 file_mode: crate::Mode,
112 /// The mode requested by the caller.
113 requested_mode: crate::Mode,
114 /// Offset where the mismatch was detected `[x, y, z]`.
115 offset: Option<[usize; 3]>,
116 },
117 /// Detailed header validation failed.
118 #[error("Invalid header: {0}")]
119 InvalidHeaderDetailed(#[from] HeaderValidationError),
120 /// Header statistics do not match the actual data.
121 #[error(
122 "Stats mismatch: header claims dmin={claimed_dmin}, dmax={claimed_dmax}, dmean={claimed_dmean}, rms={claimed_rms} but actual data has dmin={actual_dmin}, dmax={actual_dmax}, dmean={actual_dmean}, rms={actual_rms}"
123 )]
124 StatsMismatch {
125 /// Minimum density value claimed in the header.
126 claimed_dmin: f32,
127 /// Maximum density value claimed in the header.
128 claimed_dmax: f32,
129 /// Mean density value claimed in the header.
130 claimed_dmean: f32,
131 /// RMS deviation claimed in the header.
132 claimed_rms: f32,
133 /// Actual minimum density computed from the data.
134 actual_dmin: f32,
135 /// Actual maximum density computed from the data.
136 actual_dmax: f32,
137 /// Actual mean density computed from the data.
138 actual_dmean: f32,
139 /// Actual RMS deviation computed from the data.
140 actual_rms: f32,
141 },
142 /// Memory mapping failed (requires the `mmap` feature).
143 #[cfg(feature = "mmap")]
144 #[error("Memory mapping error")]
145 Mmap,
146 /// The file size does not match the header's declared data size.
147 #[error("File size mismatch: expected {expected} bytes, got {actual} bytes")]
148 FileSizeMismatch {
149 /// Expected file size in bytes (header + extended header + data).
150 expected: usize,
151 /// Actual file size in bytes.
152 actual: usize,
153 },
154 /// A volume-stack operation was requested on a file that is not a volume stack.
155 #[error("Not a volume stack: ispg={ispg}, mz={mz} (expected ispg in 401-630 with mz > 0)")]
156 NotAVolumeStack {
157 /// The ISPG (space group) value from the header.
158 ispg: i32,
159 /// The MZ (sampling along Z) value from the header.
160 mz: i32,
161 },
162 /// A value exceeds the representable range of the target type.
163 ///
164 /// Raised by [`convert_u16_slice_to_u8`](crate::convert_u16_slice_to_u8)
165 /// when a `u16` value exceeds 255.
166 #[error("Value out of range: {value} exceeds maximum of {max}")]
167 #[cfg_attr(feature = "serde", serde(skip))]
168 ValueOutOfRange {
169 /// The value that exceeded the range.
170 value: u64,
171 /// The maximum allowed value for the target type.
172 max: u64,
173 },
174}
175
176impl Error {
177 /// Create a bounds error without detailed context.
178 ///
179 /// Use this in cold error paths where the offset/shape/volume are not
180 /// immediately available. Prefer [`BoundsError`](Self::BoundsError) with
181 /// populated fields at validation boundaries.
182 #[cold]
183 pub(crate) fn bounds_err() -> Self {
184 Self::BoundsError {
185 offset: None,
186 shape: None,
187 volume: None,
188 }
189 }
190}
191
192impl From<Error> for std::io::Error {
193 fn from(err: Error) -> Self {
194 match &err {
195 Error::Io(e) => std::io::Error::new(e.kind(), err.to_string()),
196 _ => std::io::Error::other(err.to_string()),
197 }
198 }
199}
200
201/// Errors that can occur during detailed header validation.
202///
203/// These are returned by [`Header::validate_detailed`](crate::Header::validate_detailed)
204/// and surfaced through [`Error::InvalidHeaderDetailed`](crate::Error::InvalidHeaderDetailed).
205///
206/// # Example
207///
208/// ```rust
209/// use mrc::HeaderValidationError;
210///
211/// let err = HeaderValidationError::UnsupportedMode(99);
212/// assert!(err.to_string().contains("99"));
213/// ```
214#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
215#[derive(thiserror::Error, Debug, Clone, PartialEq)]
216pub enum HeaderValidationError {
217 /// One or more volume dimensions are non-positive.
218 #[error("Invalid dimensions: nx={nx}, ny={ny}, nz={nz} (must all be positive)")]
219 InvalidDimensions {
220 /// Number of columns (X axis).
221 nx: i32,
222 /// Number of rows (Y axis).
223 ny: i32,
224 /// Number of sections (Z axis).
225 nz: i32,
226 },
227 /// The mode value is not recognized.
228 #[error("Unsupported mode: {0}")]
229 UnsupportedMode(i32),
230 /// The MAP identifier field is not valid.
231 #[error("Invalid MAP field: expected 'MAP ', got {0:?}")]
232 InvalidMap([u8; 4]),
233 /// The space group number is outside the valid ranges.
234 #[error("Invalid ISPG: {0} (expected 0, 1-230, or 400-630)")]
235 InvalidIspg(i32),
236 /// The axis mapping is not a permutation of (1, 2, 3).
237 #[error(
238 "Invalid axis mapping: mapc={mapc}, mapr={mapr}, maps={maps} (must be a permutation of 1,2,3)"
239 )]
240 InvalidAxisMapping {
241 /// Column axis index.
242 mapc: i32,
243 /// Row axis index.
244 mapr: i32,
245 /// Section axis index.
246 maps: i32,
247 },
248 /// The extended header size is negative.
249 #[error("Invalid nsymbt: {0} (must be non-negative)")]
250 InvalidNsymbt(i32),
251 /// The label count is outside the range 0–10.
252 #[error("Invalid nlabl: {0} (must be between 0 and 10)")]
253 InvalidNlabl(i32),
254 /// The NVERSION value is not 0, 20140, or 20141.
255 #[error("Invalid nversion: {0} (expected 0, 20140, or 20141)")]
256 InvalidNversion(i32),
257 /// Volume stack consistency check failed.
258 #[error(
259 "Invalid volume stack: nz={nz} is not divisible by mz={mz} (required when ispg={ispg} indicates a volume stack)"
260 )]
261 InvalidVolumeStack {
262 /// Total number of sections.
263 nz: i32,
264 /// Sections per sub-volume.
265 mz: i32,
266 /// Space group number.
267 ispg: i32,
268 },
269 /// One or more sampling values are non-positive.
270 #[error("Invalid sampling: mx={mx}, my={my}, mz={mz} (must all be positive)")]
271 InvalidSampling {
272 /// Sampling along X.
273 mx: i32,
274 /// Sampling along Y.
275 my: i32,
276 /// Sampling along Z.
277 mz: i32,
278 },
279 /// The declared label count does not match the actual non-empty labels.
280 #[error("Label count mismatch: nlabl={nlabl} but {actual} non-empty labels found")]
281 LabelCountMismatch {
282 /// Declared label count in the header.
283 nlabl: i32,
284 /// Actual number of non-empty labels.
285 actual: i32,
286 },
287 /// A gap was found in the label sequence.
288 #[error("Empty label at index {index} before all filled labels")]
289 EmptyLabelBeforeFilled {
290 /// Index of the empty label slot.
291 index: i32,
292 },
293}