Range reads M0/M1 (indexed lookups, Storage trait), ZFP, in-place editing #17

Merged
osobh merged 52 commits from feat/p3-range-zfp-edit into main 2026-09-26 20:42:22 +00:00
2 changed files with 103 additions and 0 deletions
Showing only changes of commit 92c8285549 - Show all commits
+19
View File
@@ -355,6 +355,23 @@ fn read_internal_node(
pos += total_nrec_width; // skip total records in subtree
children.push((addr, child_nrec));
}
// The checksum follows the child pointers and covers the node up to it.
// Lookups prune children by the keys in this node, so an unverified
// internal node could hide a record without any error: libhdf5 refuses
// a mismatch here, and so does this.
#[cfg(feature = "checksum")]
{
ensure_len(file_data, pos, 4)?;
let stored = LittleEndian::read_u32(&file_data[pos..pos + 4]);
let computed = crate::checksum::jenkins_lookup3(&file_data[offset..pos]);
if computed != stored {
return Err(FormatError::ChecksumMismatch {
expected: stored,
computed,
});
}
}
Ok((records_start, children))
}
@@ -733,6 +750,8 @@ mod tests {
buf.extend_from_slice(&child_nrec.to_le_bytes()[..nrec_width]);
buf.resize(buf.len() + total_width, 0);
}
let sum = crate::checksum::jenkins_lookup3(&buf);
buf.extend_from_slice(&sum.to_le_bytes());
buf
}
@@ -411,3 +411,87 @@ fn links_of_every_kind_resolve_by_name_as_in_h5py() {
}
}
}
/// The link name index (v2 B-tree, record type 5) of the big group: its
/// depth and root node address, read from the one type-5 `BTHD` in the file.
fn name_index_root(bytes: &[u8]) -> (u16, usize) {
let headers: Vec<usize> = bytes
.windows(4)
.enumerate()
.filter(|(i, w)| *w == b"BTHD" && bytes.get(i + 5) == Some(&5))
.map(|(i, _)| i)
.collect();
assert_eq!(headers.len(), 1, "type-5 B-tree headers at {headers:?}");
let h = headers[0];
// signature, version, type, node size (4), record size (2), depth (2),
// split and merge percent, root address (8).
let depth = u16::from_le_bytes([bytes[h + 12], bytes[h + 13]]);
let root = u64::from_le_bytes(bytes[h + 16..h + 24].try_into().unwrap());
(depth, usize::try_from(root).unwrap())
}
/// One byte changed in a key of the name index's root (an internal node)
/// must be an error, not a name quietly routed to the wrong child and
/// reported missing: lookups prune children by those keys. libhdf5 checks
/// the internal node's checksum and refuses the group; so must we, for a
/// lookup and for a listing.
#[test]
fn a_corrupt_internal_index_node_is_an_error_not_a_missing_name() {
skip_if_no_python!();
let fx = fixture();
let mut bytes = std::fs::read(&fx.path).unwrap();
let (depth, root) = name_index_root(&bytes);
assert!(depth >= 2, "want a deep index, got depth {depth}");
assert_eq!(&bytes[root..root + 4], b"BTIN");
// Signature, version, type, then record 0: its name hash comes first.
bytes[root + 6] ^= 0x5a;
let dir = tempfile::tempdir().unwrap();
let bad = dir.path().join("bad.h5");
std::fs::write(&bad, &bytes).unwrap();
let bad = bad.display().to_string();
let is_checksum = |e: &clawhdf5::Error| {
matches!(
e,
clawhdf5::Error::Format(FormatError::ChecksumMismatch { .. })
)
};
let f = File::open(&bad).unwrap();
let g = f.group("g").unwrap();
// Every name, present or not, goes through the root.
for name in fx.links.keys().step_by(97).chain(&fx.missing_links) {
let err = g.dataset(name).map(|_| ()).unwrap_err();
assert!(is_checksum(&err), "dataset({name:?}): {err:?}");
}
let err = f.dataset("/g/n0").map(|_| ()).unwrap_err();
assert!(is_checksum(&err), "path: {err:?}");
let err = g.datasets().unwrap_err();
assert!(is_checksum(&err), "listing: {err:?}");
let err = g.entries().unwrap_err();
assert!(is_checksum(&err), "entries: {err:?}");
let m = MmapFile::open(&bad).unwrap();
let mg = m.group("g").unwrap();
assert!(mg.dataset("n0").is_err_and(|e| is_checksum(&e)));
assert!(mg.datasets().is_err_and(|e| is_checksum(&e)));
let l = LazyFile::open_mmap(&bad).unwrap();
let lg = l.group("g").unwrap();
assert!(lg.dataset("n0").is_err_and(|e| is_checksum(&e)));
assert!(lg.datasets().is_err_and(|e| is_checksum(&e)));
// libhdf5 refuses both too.
let out = run_python(&format!(
"import h5py\n\
r = []\n\
with h5py.File(r'{bad}', 'r') as f:\n\
\x20 g = f['g']\n\
\x20 for op in (lambda: g['n0'], lambda: list(g)):\n\
\x20 try:\n\
\x20 op()\n\
\x20 r.append('ok')\n\
\x20 except Exception as e:\n\
\x20 r.append('checksum' if 'checksum' in str(e) else repr(e))\n\
print(' '.join(r))",
));
assert_eq!(out, "checksum checksum");
}