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