fix(format): saturate out-of-range integer reads instead of truncating
Reading wider or differently-signed integers kept the low bits: i64 2^40+5 read as i32 was 5, u64::MAX read as i64 was -1, and -1 read as u64 was 4294967295. u32 data read as i32 also took the bulk-copy fast path meant for i32. Saturate at the target range like libhdf5's hard conversions (a negative value read as unsigned is 0), and keep the i32 fast path to signed data. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -127,3 +127,64 @@ with h5py.File("{path}", "r") as f:
|
||||
let f2 = file.dataset("f2").unwrap();
|
||||
assert_eq!(f2.read_u64().unwrap(), vec![1, 0, 0, 0, 7, 65504, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_reads_saturate_out_of_range_values_like_libhdf5() {
|
||||
// Narrowing reads used to keep the low bits (i64 2^40+5 read as i32 was
|
||||
// 5, u64::MAX read as i64 was -1) and signed-to-unsigned reads wrapped
|
||||
// (-1 read as u64 was 4294967295).
|
||||
skip_if_no_python!();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("int_narrowing.h5");
|
||||
let script = format!(
|
||||
r#"{PRELUDE}
|
||||
data = {{
|
||||
"i8": np.array([2**40 + 5, -(2**35), 7, -1, 2**63 - 1, -(2**63)], "<i8"),
|
||||
"i8be": np.array([2**40 + 5, -(2**35), 7, -1], ">i8"),
|
||||
"u8": np.array([2**64 - 1, 2**63, 5, 0], "<u8"),
|
||||
"u4": np.array([2**32 - 1, 2**31, 2**31 - 1, 3], "<u4"),
|
||||
"i4": np.array([-1, 5, -(2**31), 2**31 - 1], "<i4"),
|
||||
"i2be": np.array([-300, 300, -1], ">i2"),
|
||||
"u1": np.array([255, 0, 128], "u1"),
|
||||
}}
|
||||
with h5py.File("{path}", "w") as f:
|
||||
for name, arr in data.items():
|
||||
f.create_dataset(name, data=arr)
|
||||
with h5py.File("{path}", "r") as f:
|
||||
for name in data:
|
||||
d = f[name]
|
||||
emit(name + ":i32", d.astype("<i4")[()])
|
||||
emit(name + ":i64", d.astype("<i8")[()])
|
||||
emit(name + ":u64", d.astype("<u8")[()])
|
||||
"#,
|
||||
path = path.display()
|
||||
);
|
||||
let expected = run_python(&script);
|
||||
|
||||
let file = File::open(&path).unwrap();
|
||||
for name in ["i8", "i8be", "u8", "u4", "i4", "i2be", "u1"] {
|
||||
let ds = file.dataset(name).unwrap();
|
||||
assert_eq!(
|
||||
ds.read_i32().unwrap(),
|
||||
parse::<i32>(&expected[&format!("{name}:i32")]),
|
||||
"{name} as i32"
|
||||
);
|
||||
assert_eq!(
|
||||
ds.read_i64().unwrap(),
|
||||
parse::<i64>(&expected[&format!("{name}:i64")]),
|
||||
"{name} as i64"
|
||||
);
|
||||
// libhdf5 wraps a negative big-endian i64 read as little-endian u64
|
||||
// (it only byte-swaps when the sizes match and the order differs);
|
||||
// every other signed-to-unsigned read saturates at 0, so do that.
|
||||
if name != "i8be" {
|
||||
assert_eq!(
|
||||
ds.read_u64().unwrap(),
|
||||
parse::<u64>(&expected[&format!("{name}:u64")]),
|
||||
"{name} as u64"
|
||||
);
|
||||
}
|
||||
}
|
||||
let i8be = file.dataset("i8be").unwrap();
|
||||
assert_eq!(i8be.read_u64().unwrap(), vec![(1 << 40) + 5, 0, 7, 0]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user