fix: read compound datatypes from HDF5 1.14+/2.0 (datatype version 5)

clawhdf5 rejected datatype message version 5 for the compound class with
"invalid datatype version 5 for class 6", so it could not read compound
datasets written by modern HDF5 / h5py with libver=latest. v5 reuses the same
compact member encoding as v3/v4 (name, variable-width offset, member type), so
the parser now accepts versions 3-5 for compound.

Found by running the previously-ignored h5py interop tests against h5py 3.16 /
HDF5 2.0. Adds an h5py-free regression test using the real v5 datatype bytes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-06-03 11:55:15 +00:00
co-authored by Claude Opus 4.8
parent 3ff501c8ef
commit a13ff51918
+51 -1
View File
@@ -348,7 +348,10 @@ impl Datatype {
let num_members = (bf0 as u16) | ((bf1 as u16) << 8); let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
let mut members = Vec::with_capacity(num_members as usize); let mut members = Vec::with_capacity(num_members as usize);
if version == 3 || version == 4 { if (3..=5).contains(&version) {
// v3, v4 and v5 share the compact member encoding (name,
// variable-width offset, member datatype). HDF5 1.14+/2.0
// with `libver=latest` emits v5 compound types.
let ob = offset_bytes_for_size(size); let ob = offset_bytes_for_size(size);
for _ in 0..num_members { for _ in 0..num_members {
let (name, name_len) = read_null_terminated_string(data, pos)?; let (name, name_len) = read_null_terminated_string(data, pos)?;
@@ -1007,6 +1010,53 @@ mod tests {
} }
} }
#[test]
fn test_compound_v5_from_hdf5_2_0() {
// Real datatype message bytes emitted by h5py 3.16 / HDF5 2.0 with
// `libver=latest` for a compound dtype [('x','f8'),('y','f8'),('id','i4')].
// The wrapper is datatype version 5; members reuse the v3 compact
// encoding. Regression guard for reading modern-format compound types.
let bytes: [u8; 70] = [
0x56, 0x03, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x11, 0x20, 0x3f,
0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x34, 0x0b, 0x00, 0x34, 0xff,
0x03, 0x00, 0x00, 0x79, 0x00, 0x08, 0x11, 0x20, 0x3f, 0x00, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x40, 0x00, 0x34, 0x0b, 0x00, 0x34, 0xff, 0x03, 0x00, 0x00, 0x69, 0x64,
0x00, 0x10, 0x10, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00,
];
let (dt, _) = Datatype::parse(&bytes).unwrap();
match dt {
Datatype::Compound { size, members } => {
assert_eq!(size, 20);
assert_eq!(members.len(), 3);
assert_eq!(
(members[0].name.as_str(), members[0].byte_offset),
("x", 0)
);
assert_eq!(
(members[1].name.as_str(), members[1].byte_offset),
("y", 8)
);
assert_eq!(
(members[2].name.as_str(), members[2].byte_offset),
("id", 16)
);
assert!(matches!(
members[0].datatype,
Datatype::FloatingPoint { size: 8, .. }
));
assert!(matches!(
members[2].datatype,
Datatype::FixedPoint {
size: 4,
signed: true,
..
}
));
}
_ => panic!("expected Compound"),
}
}
#[test] #[test]
fn test_reference_object() { fn test_reference_object() {
let buf = build_dt_header(7, 1, [0, 0, 0], 8); let buf = build_dt_header(7, 1, [0, 0, 0], 8);