From c8c2930fc0827c218f6eedf1823ced258a36c5b1 Mon Sep 17 00:00:00 2001 From: osobh Date: Fri, 25 Sep 2026 21:07:02 -0500 Subject: [PATCH] fix(format): read enum and bool datasets through their base integer type read_i64/read_u64/read_i32/read_f64/read_f32 refused enumeration datatypes, including h5py's bool (an enum of int8), with a type mismatch. Read them as their base type's integer values, the way array datatypes already read through theirs. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/clawhdf5-format/src/data_read.rs | 22 +++++++---- .../tests/numeric_conversion_interop.rs | 38 +++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/clawhdf5-format/src/data_read.rs b/crates/clawhdf5-format/src/data_read.rs index 1309080..82e6544 100644 --- a/crates/clawhdf5-format/src/data_read.rs +++ b/crates/clawhdf5-format/src/data_read.rs @@ -888,9 +888,9 @@ fn native_le_to_vec(raw: &[u8], count: usize) -> Vec { /// Convert raw bytes to `f64` values. pub fn read_as_f64(raw: &[u8], datatype: &Datatype) -> Result, FormatError> { - // Array datatypes (e.g. an array-typed compound member) are read as a flat - // sequence of their base elements. - if let Datatype::Array { base_type, .. } = datatype { + // Array datatypes read as a flat sequence of their base elements, and + // enumerations (h5py's bool among them) as their integer values. + if let Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } = datatype { return read_as_f64(raw, base_type); } ensure_numeric(datatype, "FloatingPoint or FixedPoint")?; @@ -1030,7 +1030,9 @@ fn decode_scalar( /// unsigned is 0), and floating-point data is truncated toward zero and /// saturated, with NaN read as 0. pub fn read_as_i64(raw: &[u8], datatype: &Datatype) -> Result, FormatError> { - if let Datatype::Array { base_type, .. } = datatype { + // Array datatypes read as a flat sequence of their base elements, and + // enumerations (h5py's bool among them) as their integer values. + if let Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } = datatype { return read_as_i64(raw, base_type); } ensure_numeric(datatype, "FixedPoint (signed)")?; @@ -1075,7 +1077,9 @@ pub fn read_as_i64(raw: &[u8], datatype: &Datatype) -> Result, FormatEr /// unsigned is 0), and floating-point data is truncated toward zero and /// saturated, with NaN read as 0. pub fn read_as_u64(raw: &[u8], datatype: &Datatype) -> Result, FormatError> { - if let Datatype::Array { base_type, .. } = datatype { + // Array datatypes read as a flat sequence of their base elements, and + // enumerations (h5py's bool among them) as their integer values. + if let Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } = datatype { return read_as_u64(raw, base_type); } ensure_numeric(datatype, "FixedPoint (unsigned)")?; @@ -1098,7 +1102,9 @@ pub fn read_as_u64(raw: &[u8], datatype: &Datatype) -> Result, FormatEr /// Convert raw bytes to `f32` values. pub fn read_as_f32(raw: &[u8], datatype: &Datatype) -> Result, FormatError> { - if let Datatype::Array { base_type, .. } = datatype { + // Array datatypes read as a flat sequence of their base elements, and + // enumerations (h5py's bool among them) as their integer values. + if let Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } = datatype { return read_as_f32(raw, base_type); } ensure_numeric(datatype, "FloatingPoint")?; @@ -1183,7 +1189,9 @@ pub fn read_as_f32(raw: &[u8], datatype: &Datatype) -> Result, FormatEr /// unsigned is 0), and floating-point data is truncated toward zero and /// saturated, with NaN read as 0. pub fn read_as_i32(raw: &[u8], datatype: &Datatype) -> Result, FormatError> { - if let Datatype::Array { base_type, .. } = datatype { + // Array datatypes read as a flat sequence of their base elements, and + // enumerations (h5py's bool among them) as their integer values. + if let Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } = datatype { return read_as_i32(raw, base_type); } ensure_numeric(datatype, "FixedPoint")?; diff --git a/crates/clawhdf5/tests/numeric_conversion_interop.rs b/crates/clawhdf5/tests/numeric_conversion_interop.rs index 556f2a9..2777bd5 100644 --- a/crates/clawhdf5/tests/numeric_conversion_interop.rs +++ b/crates/clawhdf5/tests/numeric_conversion_interop.rs @@ -273,3 +273,41 @@ with h5py.File("{path}", "r") as f: assert_eq!(got32, want32, "{name} as f32"); } } + +#[test] +fn enum_and_bool_datasets_read_as_their_integer_values() { + // Enumerations (h5py stores bool as an enum of int8) were refused by the + // numeric readers with a type mismatch. + skip_if_no_python!(); + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("enums.h5"); + let script = format!( + r#"{PRELUDE} +with h5py.File("{path}", "w") as f: + f.create_dataset("bool", data=np.array([True, False, True])) + e = h5py.enum_dtype({{"RED": 0, "GREEN": 7, "BLUE": -3}}, basetype=">i2") + f.create_dataset("enum_i2be", data=np.array([0, 7, -3, 7], ">i2"), dtype=e) + e = h5py.enum_dtype({{"LOW": 0, "HIGH": 200}}, basetype="u1") + f.create_dataset("enum_u1", data=np.array([200, 0, 200], "u1"), dtype=e) + e = h5py.enum_dtype({{"A": -(2**40), "B": 2**40}}, basetype=" = parse(&expected[name]); + assert_eq!(ds.read_i64().unwrap(), want, "{name} as i64"); + let want_f64: Vec = want.iter().map(|&v| v as f64).collect(); + assert_eq!(ds.read_f64().unwrap(), want_f64, "{name} as f64"); + } + let bools = file.dataset("bool").unwrap(); + assert_eq!(bools.read_u64().unwrap(), vec![1, 0, 1]); + assert_eq!(bools.read_i32().unwrap(), vec![1, 0, 1]); +}