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
Showing only changes of commit 95dcb04454 - Show all commits
+53 -1
View File
@@ -246,8 +246,20 @@ fn scaleoffset_decompress(
fill_value fill_value
} else if is_escale { } else if is_escale {
minval + code as f64 * powi_f64(2.0, scale_factor) minval + code as f64 * powi_f64(2.0, scale_factor)
} else if elem_size == 4 {
// H5Z_scaleoffset_modify_3/4 for `float`: the code is
// read as an `int` and everything is single precision,
// `(float)code / powf(10, D) + min`. Doing it in f64 and
// rounding once at the end is off by 1 ULP at times.
let d = if scale_factor >= 0 {
powi_f64(10.0, scale_factor) as f32
} else {
1.0 / powi_f64(10.0, -scale_factor) as f32
};
((code as u32 as i32) as f32 / d + minval as f32) as f64
} else { } else {
minval + code as f64 / powi_f64(10.0, scale_factor) // ... and for `double`: `(double)(long)code / pow(10, D) + min`.
(code as i64) as f64 / powi_f64(10.0, scale_factor) + minval
} }
}) })
.collect(); .collect();
@@ -1630,6 +1642,46 @@ mod tests {
} }
} }
/// `le_data.h5` scale-offset (D-scale, D = 3, fill -2.2) chunks, decoded
/// bit for bit as libhdf5 does: single-precision arithmetic for `float`
/// (we computed in f64 and rounded once, which was 1 ULP off for e.g.
/// 1.6663333: `694ad53f` instead of `6a4ad53f`), double for `double`.
#[test]
fn scaleoffset_float_dscale_matches_libhdf5_bits() {
let file: &[u8] = include_bytes!("../tests/fixtures/filters/le_data.h5");
let cd = |size: u32, order: u32, fill_lo: u32, fill_hi: u32| {
let mut cd = vec![0, 3, 12, 1, size, 0, order, 1, fill_lo, fill_hi];
cd.resize(20, 0);
cd
};
let f32_le = cd(4, 0, 0xC00C_CCCD, 0);
let f32_be = cd(4, 1, 0xC00C_CCCD, 0);
let f64_le = cd(8, 0, 2576980378, 3221330329);
#[rustfmt::skip]
let cases: [(usize, usize, &[u32], &str); 6] = [
(2816, 38, &f32_le, "abaaaa3ed2942a3fec0a803fd2942a3fec0a803fabaaaa3fec0a803fabaaaa3f694ad53fabaaaa3f694ad53f76050040"),
(2854, 38, &f32_le, "abaaaa3f6a4ad53f760500406a4ad53f7605004056551540760500405655154034a52a405655154034a52a4076054040"),
(712, 38, &f32_be, "3eaaaaab3f2a94d23f800aec3f2a94d23f800aec3faaaaab3f800aec3faaaaab3fd54a693faaaaab3fd54a6940000576"),
(750, 38, &f32_be, "3faaaaab3fd54a6a400005763fd54a6a40000576401555564000057640155556402aa53440155556402aa53440400576"),
(2050, 38, &f64_le, concat!(
"555555555555d53fb9d75c489a52e53fce3e7c865d01f03fb9d75c489a52e53fce3e7c865d01f03f555555555555f53f",
"ce3e7c865d01f03f555555555555f53fdc6b2e244da9fa3f555555555555f53fdc6b2e244da9fa3f671f3ec3ae000040")),
(2088, 38, &f64_le, concat!(
"555555555555f53fdc6b2e244da9fa3f671f3ec3ae000040dc6b2e244da9fa3f671f3ec3ae000040aaaaaaaaaaaa0240",
"671f3ec3ae000040aaaaaaaaaaaa0240ee351792a6540540aaaaaaaaaaaa0240ee351792a6540540671f3ec3ae000840")),
];
for (off, len, cd, want) in cases {
let pipeline = FilterPipeline {
version: 2,
filters: vec![one_filter(FILTER_SCALEOFFSET, cd.to_vec())],
};
let want = unhex(want);
let got =
decompress_chunk(&file[off..off + len], &pipeline, want.len(), cd[4]).unwrap();
assert_eq!(got, want, "chunk at {off}");
}
}
/// `le_data.h5` `/Nbit_float_data_{le,be}` chunk (0,0): a 20-bit float /// `le_data.h5` `/Nbit_float_data_{le,be}` chunk (0,0): a 20-bit float
/// (offset 7) packed by N-Bit. The filter must reproduce libhdf5's /// (offset 7) packed by N-Bit. The filter must reproduce libhdf5's
/// decoded bytes in the *file* datatype (h5py `DatasetID.read` with the /// decoded bytes in the *file* datatype (h5py `DatasetID.read` with the