Skip to main content

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}