Skip to main content

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}