hdf5_pure/mat/error.rs
1//! Error type for MATLAB v7.3 serde (de)serialization.
2
3use core::fmt;
4
5use crate::error::{Error as Hdf5Error, FormatError};
6
7/// Errors that can occur when (de)serializing `.mat` v7.3 files.
8///
9/// Marked `#[non_exhaustive]`: reading MATLAB's MCOS opaque classes is an
10/// ongoing effort (`datetime`, `categorical`, `table`, `containers.Map`,
11/// `dictionary`, …), and each newly decoded — or newly refused — class can
12/// introduce a more specific error variant. Keeping the enum open lets those
13/// additions land without a breaking change, so downstream `match`es must
14/// include a wildcard arm.
15#[derive(Debug)]
16#[non_exhaustive]
17pub enum MatError {
18 /// Underlying HDF5 I/O or format error.
19 Hdf5(Hdf5Error),
20 /// Underlying HDF5 format parse error.
21 Format(FormatError),
22 /// I/O error when reading or writing a file path.
23 Io(std::io::Error),
24 /// Top-level must be a struct with named fields (each field becomes a MATLAB variable).
25 RootMustBeStruct,
26 /// The requested Rust type has no MATLAB v7.3 encoding in this crate.
27 UnsupportedType(&'static str),
28 /// A sequence contained elements of different primitive types.
29 MixedSequenceElementTypes,
30 /// A dataset's on-disk shape didn't match the Rust type.
31 ShapeMismatch {
32 /// The Rust side's expectation.
33 expected: String,
34 /// What the file contained.
35 actual: String,
36 },
37 /// A required struct field was missing from the file.
38 MissingField(String),
39 /// A `MATLAB_class` attribute value wasn't recognized.
40 UnknownClass(String),
41 /// A recognized but not-yet-supported MATLAB class was encountered on read
42 /// — an MCOS opaque class (`datetime`, `categorical`, `table`,
43 /// `containers.Map`, `dictionary`, an enumeration, a user `classdef`, …)
44 /// whose decoder is not yet implemented. Refused by name rather than
45 /// misread; the modern `string` class is supported.
46 UnsupportedMatlabClass(String),
47 /// UTF-16 decoding of a `char` dataset failed.
48 Utf16Decode(String),
49 /// A [`DataProducer`](crate::mat::DataProducer) wrote the wrong number of
50 /// bytes for a block. Refused rather than written: a block of the wrong size
51 /// displaces every address after it, and the result would be a file that
52 /// fails to open for reasons that no longer point back here.
53 BlockSizeMismatch {
54 /// Block index the producer was asked for.
55 block: usize,
56 /// Bytes it had to write, as
57 /// [`Blocking::block_len`](crate::mat::Blocking::block_len) reports.
58 expected: usize,
59 /// Bytes it actually wrote.
60 actual: usize,
61 },
62 /// A producer-backed dataset was asked for on a builder configured for
63 /// compression. The layout needs each block's exact on-disk size before it
64 /// writes anything, and a compressed block's size is not knowable without
65 /// compressing it — which would buffer the data the path exists to avoid.
66 CompressionUnsupportedForBlocks,
67 /// [`Options::compression`](crate::mat::Options::compression) was set
68 /// alongside an [`Options::libver`](crate::mat::Options::libver) too old to
69 /// carry it.
70 ///
71 /// Compression needs chunked storage, and the chunk indices this crate
72 /// writes arrived in HDF5 1.10 — while the MAT default is the 1.8 format,
73 /// because MATLAB used HDF5 1.8.12 before R2021b. Refused rather than
74 /// resolved either way: dropping the compression loses what the caller asked
75 /// for, and raising the format produces a `.mat` file MATLAB cannot `load`.
76 /// Set `libver` to [`LibVer::V110`](crate::LibVer::V110) to compress and
77 /// accept the newer format.
78 CompressionNeedsNewerFormat,
79 /// A generic serde-originated error (from `Error::custom`).
80 Custom(String),
81 /// An error from the calling crate, carried whole.
82 ///
83 /// The builder's nesting closures
84 /// ([`MatBuilder::struct_`](crate::mat::MatBuilder::struct_),
85 /// [`MatBuilder::cell`](crate::mat::MatBuilder::cell),
86 /// [`CellWriter::push_with`](crate::mat::CellWriter::push_with) and their
87 /// siblings) and
88 /// [`DataProducer::block_bytes`](crate::mat::DataProducer::block_bytes)
89 /// return `Result<(), MatError>`, so a crate that emits `.mat` files as one
90 /// of several formats has to put its own error type through that boundary.
91 /// [`Custom`](MatError::Custom) keeps only the `Display` text; this keeps
92 /// the error, so the caller's caller can still `downcast_ref` it back out
93 /// of [`source`](std::error::Error::source):
94 ///
95 /// ```
96 /// # use hdf5_pure::mat::{MatBuilder, MatError, Options};
97 /// # use std::error::Error;
98 /// # #[derive(Debug)]
99 /// # struct EncodeError(&'static str);
100 /// # impl std::fmt::Display for EncodeError {
101 /// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 /// # write!(f, "{}", self.0)
103 /// # }
104 /// # }
105 /// # impl Error for EncodeError {}
106 /// # fn encode() -> Result<u32, EncodeError> { Err(EncodeError("no MAT encoding")) }
107 /// let mut mb = MatBuilder::new(Options::default());
108 /// let err = mb
109 /// .struct_("payload", |s| {
110 /// let value = encode().map_err(MatError::from_source)?;
111 /// s.write_scalar_u32("value", value)?;
112 /// Ok(())
113 /// })
114 /// .err()
115 /// .expect("the closure failed");
116 ///
117 /// let original = err.source().unwrap().downcast_ref::<EncodeError>().unwrap();
118 /// assert_eq!(original.0, "no MAT encoding");
119 /// ```
120 ///
121 /// `'static` is what `source` hands back. `Send + Sync` is what the crate
122 /// already needs of a `MatError`: a failed producer's error waits in an
123 /// `Arc<Mutex<_>>` for the finalizer to swap it back in, and that is what
124 /// keeps `MatBuilder` itself `Send + Sync`.
125 ///
126 /// `Display` prints the inner error, which a formatter that walks the whole
127 /// source chain will therefore print twice. That matches
128 /// [`std::io::Error`]'s behaviour for the same case.
129 Source(Box<dyn std::error::Error + Send + Sync + 'static>),
130}
131
132impl MatError {
133 /// Carry an error from the calling crate whole, as [`MatError::Source`].
134 ///
135 /// Shaped after `std::io::Error::other`: it takes a concrete error type or
136 /// an already-boxed one. Reach for it at a builder closure's edge, where
137 /// `.map_err(MatError::from_source)` reads as a one-word conversion.
138 ///
139 /// The bound also admits a `String`, which the conversion accepts and
140 /// nothing can recover: `downcast_ref` needs a type that implements
141 /// `Error`, and `String` does not. A bare message belongs in
142 /// [`MatError::Custom`].
143 pub fn from_source<E>(source: E) -> Self
144 where
145 E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
146 {
147 MatError::Source(source.into())
148 }
149}
150
151impl fmt::Display for MatError {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 match self {
154 MatError::Hdf5(e) => write!(f, "HDF5 error: {e}"),
155 MatError::Format(e) => write!(f, "HDF5 format error: {e}"),
156 MatError::Io(e) => write!(f, "I/O error: {e}"),
157 MatError::RootMustBeStruct => write!(
158 f,
159 "top-level value must be a struct with named fields; each field becomes a MATLAB variable"
160 ),
161 MatError::UnsupportedType(t) => write!(f, "unsupported Rust type for MAT v7.3: {t}"),
162 MatError::MixedSequenceElementTypes => write!(
163 f,
164 "sequence elements have mixed primitive types; all elements of a numeric array must share a type"
165 ),
166 MatError::ShapeMismatch { expected, actual } => {
167 write!(f, "shape mismatch: expected {expected}, got {actual}")
168 }
169 MatError::MissingField(name) => write!(f, "missing required field: {name}"),
170 MatError::UnknownClass(c) => write!(f, "unknown MATLAB_class: {c:?}"),
171 MatError::UnsupportedMatlabClass(c) => write!(
172 f,
173 "MATLAB class {c:?} is not yet supported for reading (modern `string` is; \
174 other MCOS opaque classes such as datetime/categorical/table are refused for now)"
175 ),
176 MatError::Utf16Decode(msg) => write!(f, "UTF-16 decode: {msg}"),
177 MatError::BlockSizeMismatch {
178 block,
179 expected,
180 actual,
181 } => write!(
182 f,
183 "block producer wrote {actual} bytes for block {block}, which must carry exactly \
184 {expected}"
185 ),
186 MatError::CompressionUnsupportedForBlocks => write!(
187 f,
188 "a producer-backed dataset cannot be compressed: its blocks' on-disk sizes must be \
189 known before the file is laid out"
190 ),
191 MatError::CompressionNeedsNewerFormat => write!(
192 f,
193 "compression needs chunked storage, which needs the HDF5 1.10 format, but \
194 Options::libver asks for 1.8 so MATLAB's MAT v7.3 loader can read the file; \
195 set libver to LibVer::V110 to compress"
196 ),
197 MatError::Custom(msg) => write!(f, "{msg}"),
198 MatError::Source(e) => write!(f, "{e}"),
199 }
200 }
201}
202
203impl std::error::Error for MatError {
204 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
205 match self {
206 MatError::Hdf5(e) => Some(e),
207 MatError::Format(e) => Some(e),
208 MatError::Io(e) => Some(e),
209 MatError::Source(e) => Some(&**e),
210 _ => None,
211 }
212 }
213}
214
215impl From<Hdf5Error> for MatError {
216 fn from(e: Hdf5Error) -> Self {
217 MatError::Hdf5(e)
218 }
219}
220
221impl From<FormatError> for MatError {
222 fn from(e: FormatError) -> Self {
223 MatError::Format(e)
224 }
225}
226
227impl From<std::io::Error> for MatError {
228 fn from(e: std::io::Error) -> Self {
229 MatError::Io(e)
230 }
231}
232
233#[cfg(feature = "serde")]
234impl serde::ser::Error for MatError {
235 fn custom<T: fmt::Display>(msg: T) -> Self {
236 MatError::Custom(msg.to_string())
237 }
238}
239
240#[cfg(feature = "serde")]
241impl serde::de::Error for MatError {
242 fn custom<T: fmt::Display>(msg: T) -> Self {
243 MatError::Custom(msg.to_string())
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::error::Error;
251
252 #[derive(Debug, PartialEq)]
253 struct EmbedderError {
254 code: u32,
255 }
256
257 impl fmt::Display for EmbedderError {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 write!(f, "embedder failed with code {}", self.code)
260 }
261 }
262
263 impl Error for EmbedderError {}
264
265 #[test]
266 fn a_carried_error_downcasts_back_to_its_own_type() {
267 let err = MatError::from_source(EmbedderError { code: 7 });
268
269 let source = err.source().expect("Source carries its error");
270 assert_eq!(
271 source.downcast_ref::<EmbedderError>(),
272 Some(&EmbedderError { code: 7 }),
273 "the whole point of the variant: the type survives the boundary"
274 );
275 }
276
277 #[test]
278 fn a_carried_error_displays_as_itself() {
279 let err = MatError::from_source(EmbedderError { code: 7 });
280 assert_eq!(err.to_string(), "embedder failed with code 7");
281 }
282
283 #[test]
284 fn an_already_boxed_error_is_accepted_whole() {
285 // `Box<dyn Error + Send + Sync>` does not itself implement `Error`, so a
286 // bound of `E: Error` would refuse exactly the embedder that had already
287 // erased its own type. `Into<Box<...>>` takes both.
288 let boxed: Box<dyn Error + Send + Sync + 'static> = Box::new(EmbedderError { code: 7 });
289 let err = MatError::from_source(boxed);
290
291 assert!(
292 err.source()
293 .and_then(|s| s.downcast_ref::<EmbedderError>())
294 .is_some()
295 );
296 }
297
298 #[test]
299 fn the_error_type_is_still_send_and_sync() {
300 // A `Box<dyn Error>` without these bounds would revoke both, and the
301 // first thing to break is a `MatBuilder` holding a producer's stashed
302 // failure. This states the property where it lives, so the failure
303 // names the error type rather than the builder three modules away.
304 fn assert_send_sync<T: Send + Sync>() {}
305 assert_send_sync::<MatError>();
306 }
307}