format: chunk grids with a zero extent have no chunks (no division by zero)

A Fixed or Extensible Array index whose maximum extent (the current one
when no maximum is recorded) is 0 along a dimension has a zero stride for
every dimension before it; ChunkGrid::offsets divided by it. The unfixed
editor made such files by resizing a clawhdf5-written dataset to a zero
extent: `h5rs check` panicked and the next resize raised an internal
error (12 of the reviewer's random-edit seeds 10..39). Such an index has
no slot for any chunk of the dataset; offsets now returns None.

Tests: chunk_grid::zero_extent_has_no_chunks; edit_interop's
zero_extent_resizes_without_a_recorded_maximum on a file the unfixed
editor left (fixture) and on a 2.7.0-written file taken through zero
extents with `h5rs check --data` and h5dump at every step; test_edit.py
random edits on seeds 10..39 of a clawhdf5-written file.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-27 07:40:11 -05:00
co-authored by Claude Opus 5.5
parent 1f7651644b
commit bdf584abb2
5 changed files with 116 additions and 0 deletions
@@ -1509,3 +1509,68 @@ fn unencodable_filters_are_unsupported() {
"a refused edit changed the file"
);
}
fn fixture(dir: &Path, name: &str) -> std::path::PathBuf {
let path = dir.join(name);
std::fs::copy(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../clawhdf5/tests/fixtures")
.join(name),
&path,
)
.unwrap();
path
}
/// Zero extents on chunked datasets with no recorded maximum. The unfixed
/// editor left `chunk_zero_extent_no_maxshape.h5` (a 2.7.0-written file
/// resized to 1x0): a Fixed Array whose maximum, taken from the current
/// dimensions, has no chunks along one dimension, so every stride before it
/// is 0 — `h5rs check` panicked dividing by it and the next resize failed
/// with an internal error. Such a file must check clean and resize on; a
/// 2.7.0-written file taken through zero extents by the fixed editor must
/// check clean at every step and read the fill value where it grew.
#[test]
fn zero_extent_resizes_without_a_recorded_maximum() {
if !tools_ok() {
return;
}
let dir = tmpdir();
let path = fixture(dir.path(), "chunk_zero_extent_no_maxshape.h5");
check_tools(&path, true);
let mut ed = FileEditor::open(&path).unwrap();
ed.resize("d", &[0, 0]).unwrap();
ed.resize("z", &[0, 0]).unwrap();
// Their maximum is now what the index was laid out by (1 x 0).
ed.resize("d", &[1, 0]).unwrap();
assert!(matches!(
ed.resize("d", &[1, 1]),
Err(Error::InvalidArgument(_))
));
drop(ed);
check_tools(&path, true);
let path = fixture(dir.path(), "chunked_no_maxshape_v2_7_0.h5");
for shape in [[15, 15], [3, 2], [1, 1], [1, 0], [0, 0], [0, 20], [20, 20]] {
let mut ed = FileEditor::open(&path).unwrap();
ed.resize("d", &shape).unwrap();
ed.resize("z", &shape).unwrap();
drop(ed);
check_tools(&path, true);
}
let f = File::open(&path).unwrap();
for name in ["d", "z"] {
let d = f.dataset(name).unwrap();
assert_eq!(d.shape().unwrap(), [20, 20]);
assert!(d.read_f32().unwrap().iter().all(|&v| v == 0.0), "{name}");
}
assert_eq!(
py(&format!(
"import h5py\n\
with h5py.File({:?}) as f:\n\
\x20 print(int(abs(f['d'][()]).sum() + abs(f['z'][()]).sum()), f['d'].maxshape)",
path.to_str().unwrap()
)),
"0 (20, 20)"
);
}