fix: read array datatypes and chunked layouts from HDF5 2.0 (version 5)

Follow-up to the v5 compound fix, found by an interop sweep over diverse
h5py/HDF5 2.0 (libver=latest) datasets:

- Array datatype (class 10) version 5 was rejected. v3/v4/v5 share the same
  array encoding, so the parser now accepts 3-5.
- Data Layout message version 5 was rejected, which broke EVERY chunked/
  compressed dataset written by modern HDF5. v5 reuses the v4 message
  structure, so it now routes through parse_v4.

Validated end-to-end: a gzip-compressed, Fixed-Array-indexed v5 chunked dataset
now decodes to the correct values. Adds h5py-free regression tests using the
real v5 array-datatype and chunked-layout bytes, and updates the layout
invalid-version test to use v6.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-06-03 12:00:38 +00:00
co-authored by Claude Opus 4.8
parent a13ff51918
commit bc3a3a977a
2 changed files with 56 additions and 4 deletions
+30 -3
View File
@@ -250,7 +250,9 @@ impl DataLayout {
match version { match version {
3 => Self::parse_v3(data, layout_class, offset_size, length_size), 3 => Self::parse_v3(data, layout_class, offset_size, length_size),
4 => Self::parse_v4(data, layout_class, offset_size, length_size), // v5 (emitted by HDF5 1.14+/2.0 with `libver=latest`) uses the same
// message structure as v4 — only the version number was bumped.
4 | 5 => Self::parse_v4(data, layout_class, offset_size, length_size),
_ => Err(FormatError::InvalidLayoutVersion(version)), _ => Err(FormatError::InvalidLayoutVersion(version)),
} }
} }
@@ -626,6 +628,30 @@ mod tests {
); );
} }
#[test]
fn v5_chunked_from_hdf5_2_0() {
// Real data layout message from h5py 3.16 / HDF5 2.0 (`libver=latest`)
// for a gzip-compressed 1-D chunked dataset. Version 5 uses the same
// structure as v4 (here: chunked, Fixed Array index). Regression guard
// for reading modern-format chunked datasets.
let bytes: [u8; 17] = [
0x05, 0x02, 0x00, 0x02, 0x01, 0x0a, 0x08, 0x03, 0x0a, 0xef, 0x05, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
];
let layout = DataLayout::parse(&bytes, 8, 8).unwrap();
match layout {
DataLayout::Chunked {
chunk_dimensions,
chunk_index_type,
..
} => {
assert_eq!(chunk_dimensions, vec![10, 8]);
assert_eq!(chunk_index_type, Some(3)); // Fixed Array
}
other => panic!("expected Chunked, got {other:?}"),
}
}
#[test] #[test]
fn v4_chunked_single_chunk_no_filters() { fn v4_chunked_single_chunk_no_filters() {
let mut buf = vec![4u8, 2]; // version=4, class=2 let mut buf = vec![4u8, 2]; // version=4, class=2
@@ -678,9 +704,10 @@ mod tests {
#[test] #[test]
fn invalid_version() { fn invalid_version() {
let buf = vec![5u8, 0, 0, 0]; // v3-v5 are supported; v6 is not a real layout message version.
let buf = vec![6u8, 0, 0, 0];
let err = DataLayout::parse(&buf, 8, 8).unwrap_err(); let err = DataLayout::parse(&buf, 8, 8).unwrap_err();
assert_eq!(err, FormatError::InvalidLayoutVersion(5)); assert_eq!(err, FormatError::InvalidLayoutVersion(6));
} }
#[test] #[test]
+26 -1
View File
@@ -503,7 +503,9 @@ impl Datatype {
}, },
pos, pos,
)) ))
} else if version == 3 { } else if (3..=5).contains(&version) {
// v3, v4 and v5 share the array encoding (ndims, dims, base
// type); HDF5 1.14+/2.0 with `libver=latest` emits v5.
ensure_len(data, pos, 1)?; ensure_len(data, pos, 1)?;
let ndims = data[pos] as usize; let ndims = data[pos] as usize;
pos += 1; pos += 1;
@@ -1057,6 +1059,29 @@ mod tests {
} }
} }
#[test]
fn test_array_v5_from_hdf5_2_0() {
// Real datatype message from h5py 3.16 / HDF5 2.0 (`libver=latest`) for
// an array dtype `('f8', (3,))`: datatype version 5, class 10, reusing
// the v3 array encoding (ndims, dims, base type).
let bytes: [u8; 33] = [
0x5a, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x11,
0x20, 0x3f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x34, 0x0b, 0x00,
0x34, 0xff, 0x03, 0x00, 0x00,
];
let (dt, _) = Datatype::parse(&bytes).unwrap();
match dt {
Datatype::Array {
base_type,
dimensions,
} => {
assert_eq!(dimensions, vec![3]);
assert!(matches!(*base_type, Datatype::FloatingPoint { size: 8, .. }));
}
other => panic!("expected Array, got {other:?}"),
}
}
#[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);