fix(format): page Fixed Array data blocks past 1024 chunks

The Fixed Array writer always packed every element into one data block
behind one checksum. Past 2^10 elements libhdf5 (and our reader) expect a
paged block: a page-init bitmap after the prefix, then one checksummed page
per 1024 elements. Any dataset with more than 1024 chunks and no unlimited
dimension failed with "incorrect metadata checksum" in h5py, h5dump and
our own reader.

build_fixed_array_at now takes one Option<WrittenChunk> per array slot so
later fixes can leave unallocated slots.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-25 21:07:08 -05:00
co-authored by Claude Opus 5.5
parent bba1560416
commit 4a1876faf2
2 changed files with 240 additions and 70 deletions
+137 -1
View File
@@ -11,7 +11,7 @@
use std::process::Command;
use clawhdf5::File;
use clawhdf5::{File, FileBuilder};
fn python() -> String {
std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string())
@@ -254,3 +254,139 @@ fn h5py_fixed_array_partial_extent_reads_correctly() {
},
]);
}
// ===========================================================================
// Files we write, read back by libhdf5 (h5py and h5dump) and by us
// ===========================================================================
/// One `i4` dataset we write, filled with `arange` over `shape`.
struct WriteCase {
name: String,
shape: Vec<u64>,
chunks: Vec<u64>,
maxshape: Option<Vec<u64>>,
deflate: bool,
}
fn wcase(name: &str, shape: &[u64], chunks: &[u64], maxshape: Option<&[u64]>) -> WriteCase {
WriteCase {
name: name.to_string(),
shape: shape.to_vec(),
chunks: chunks.to_vec(),
maxshape: maxshape.map(<[u64]>::to_vec),
deflate: false,
}
}
fn h5dump_available() -> bool {
Command::new("h5dump")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Write every case into one file with our writer, then check that our own
/// reader, h5py and h5dump (when installed) all return every value. Only the
/// libhdf5 half is skipped without h5py.
fn check_we_write(cases: &[WriteCase]) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ours_chunk_index.h5");
let path_str = path.display().to_string();
let mut b = FileBuilder::new();
for c in cases {
let n: u64 = c.shape.iter().product();
let data: Vec<i32> = (0..n as i32).collect();
let ds = b.create_dataset(&c.name);
ds.with_i32_data(&data)
.with_shape(&c.shape)
.with_chunks(&c.chunks);
if let Some(ms) = &c.maxshape {
ds.with_maxshape(ms);
}
if c.deflate {
ds.with_deflate(4);
}
}
b.write(&path).unwrap();
// Our reader.
let file = File::open(&path).unwrap();
for c in cases {
let got = file.dataset(&c.name).unwrap().read_i32().unwrap();
let n: u64 = c.shape.iter().product();
let bad = got
.iter()
.enumerate()
.filter(|&(i, &v)| v != i as i32)
.count();
assert!(
got.len() == n as usize && bad == 0,
"{}: our reader: {bad} of {n} values wrong",
c.name
);
}
// libhdf5 via h5py.
skip_if_no_python!();
let mut script =
format!("import h5py, numpy as np\nbad = []\nf = h5py.File(r'{path_str}', 'r')\n");
for c in cases {
let shape: Vec<String> = c.shape.iter().map(u64::to_string).collect();
let maxshape: Vec<String> = c
.maxshape
.as_ref()
.unwrap_or(&c.shape)
.iter()
.map(|&d| {
if d == u64::MAX {
"None".to_string()
} else {
d.to_string()
}
})
.collect();
script += &format!(
"d = f['{name}']\n\
want = np.arange({n}, dtype='i4').reshape(({shape},))\n\
got = d[()]\n\
if d.maxshape != ({maxshape},): bad.append(('{name}', 'maxshape', d.maxshape))\n\
elif not np.array_equal(got, want): \
bad.append(('{name}', int((got != want).sum()), 'of', got.size))\n",
name = c.name,
n = c.shape.iter().product::<u64>(),
shape = shape.join(","),
maxshape = maxshape.join(","),
);
}
script += "print(bad if bad else 'OK')\n";
let out = run_python(&script);
assert_eq!(out, "OK", "h5py disagrees");
// libhdf5's own tool, when installed.
if h5dump_available() {
let o = Command::new("h5dump").arg(&path).output().unwrap();
let stderr = String::from_utf8_lossy(&o.stderr);
assert!(
o.status.success() && !stderr.to_lowercase().contains("error"),
"h5dump failed: {stderr}"
);
}
}
/// A Fixed Array with more than 1024 elements must be paged, or libhdf5
/// rejects the data block's checksum.
#[test]
fn we_write_paged_fixed_array() {
let mut cases: Vec<WriteCase> = [1023u64, 1024, 1025, 2048, 5000]
.iter()
.map(|&n| wcase(&format!("fa_{n}"), &[n * 4], &[4], None))
.collect();
// Filtered elements are wider; a 2-D grid pages the same way.
let mut filtered = wcase("fa_1500_deflate", &[1500 * 4], &[4], None);
filtered.deflate = true;
cases.push(filtered);
cases.push(wcase("fa_2d_1100", &[110, 40], &[1, 4], None));
check_we_write(&cases);
}