Fix silent wrong data and libhdf5 interop found by the HDF5 audit #11

Merged
osobh merged 41 commits from fix/phase0-correctness into main 2026-09-26 02:42:54 +00:00
2 changed files with 53 additions and 7 deletions
Showing only changes of commit c8c2930fc0 - Show all commits
+15 -7
View File
@@ -888,9 +888,9 @@ fn native_le_to_vec<T: Copy>(raw: &[u8], count: usize) -> Vec<T> {
/// Convert raw bytes to `f64` values. /// Convert raw bytes to `f64` values.
pub fn read_as_f64(raw: &[u8], datatype: &Datatype) -> Result<Vec<f64>, FormatError> { pub fn read_as_f64(raw: &[u8], datatype: &Datatype) -> Result<Vec<f64>, FormatError> {
// Array datatypes (e.g. an array-typed compound member) are read as a flat // Array datatypes read as a flat sequence of their base elements, and
// sequence of their base elements. // enumerations (h5py's bool among them) as their integer values.
if let Datatype::Array { base_type, .. } = datatype { if let Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } = datatype {
return read_as_f64(raw, base_type); return read_as_f64(raw, base_type);
} }
ensure_numeric(datatype, "FloatingPoint or FixedPoint")?; 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 /// unsigned is 0), and floating-point data is truncated toward zero and
/// saturated, with NaN read as 0. /// saturated, with NaN read as 0.
pub fn read_as_i64(raw: &[u8], datatype: &Datatype) -> Result<Vec<i64>, FormatError> { pub fn read_as_i64(raw: &[u8], datatype: &Datatype) -> Result<Vec<i64>, 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); return read_as_i64(raw, base_type);
} }
ensure_numeric(datatype, "FixedPoint (signed)")?; ensure_numeric(datatype, "FixedPoint (signed)")?;
@@ -1075,7 +1077,9 @@ pub fn read_as_i64(raw: &[u8], datatype: &Datatype) -> Result<Vec<i64>, FormatEr
/// unsigned is 0), and floating-point data is truncated toward zero and /// unsigned is 0), and floating-point data is truncated toward zero and
/// saturated, with NaN read as 0. /// saturated, with NaN read as 0.
pub fn read_as_u64(raw: &[u8], datatype: &Datatype) -> Result<Vec<u64>, FormatError> { pub fn read_as_u64(raw: &[u8], datatype: &Datatype) -> Result<Vec<u64>, 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); return read_as_u64(raw, base_type);
} }
ensure_numeric(datatype, "FixedPoint (unsigned)")?; ensure_numeric(datatype, "FixedPoint (unsigned)")?;
@@ -1098,7 +1102,9 @@ pub fn read_as_u64(raw: &[u8], datatype: &Datatype) -> Result<Vec<u64>, FormatEr
/// Convert raw bytes to `f32` values. /// Convert raw bytes to `f32` values.
pub fn read_as_f32(raw: &[u8], datatype: &Datatype) -> Result<Vec<f32>, FormatError> { pub fn read_as_f32(raw: &[u8], datatype: &Datatype) -> Result<Vec<f32>, 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); return read_as_f32(raw, base_type);
} }
ensure_numeric(datatype, "FloatingPoint")?; ensure_numeric(datatype, "FloatingPoint")?;
@@ -1183,7 +1189,9 @@ pub fn read_as_f32(raw: &[u8], datatype: &Datatype) -> Result<Vec<f32>, FormatEr
/// unsigned is 0), and floating-point data is truncated toward zero and /// unsigned is 0), and floating-point data is truncated toward zero and
/// saturated, with NaN read as 0. /// saturated, with NaN read as 0.
pub fn read_as_i32(raw: &[u8], datatype: &Datatype) -> Result<Vec<i32>, FormatError> { pub fn read_as_i32(raw: &[u8], datatype: &Datatype) -> Result<Vec<i32>, 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); return read_as_i32(raw, base_type);
} }
ensure_numeric(datatype, "FixedPoint")?; ensure_numeric(datatype, "FixedPoint")?;
@@ -273,3 +273,41 @@ with h5py.File("{path}", "r") as f:
assert_eq!(got32, want32, "{name} as f32"); 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="<i8")
f.create_dataset("enum_i8", data=np.array([2**40, -(2**40)], "<i8"), dtype=e)
with h5py.File("{path}", "r") as f:
for name in ("bool", "enum_i2be", "enum_u1", "enum_i8"):
emit(name, np.asarray(f[name][()]).astype(np.int64))
"#,
path = path.display()
);
let expected = run_python(&script);
let file = File::open(&path).unwrap();
for name in ["bool", "enum_i2be", "enum_u1", "enum_i8"] {
let ds = file.dataset(name).unwrap();
let want: Vec<i64> = parse(&expected[name]);
assert_eq!(ds.read_i64().unwrap(), want, "{name} as i64");
let want_f64: Vec<f64> = 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]);
}