From 1ffd013de910012444d89b648fb082154551ee84 Mon Sep 17 00:00:00 2001 From: osobh Date: Sat, 26 Sep 2026 17:11:36 -0500 Subject: [PATCH] edit: delete a heap's huge-object B-tree with its last huge object libhdf5 deletes a fractal heap's huge-object B-tree when the heap is closed with no huge object left (H5HF__huge_term) and starts huge IDs over. The editor left the empty tree, and a read-only libhdf5 then failed to list the object's attributes: closing the heap tried to delete the tree ("no write intent on file"), and h5dump failed the same way. Replacing an object's last huge attribute (one above the heap's 4 KiB managed limit) now deletes the tree (header and nodes freed), resets the next huge ID and the wrapped flag, as libhdf5 does; a later huge attribute creates a new tree. Bt2::delete frees a whole tree. Found by the extended random-operation test; regression: last_huge_attribute_replaced (fails before: h5dump, h5py listing), which also compares the heap with libhdf5's after the same replacement. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/edit_coverage_interop.rs | 54 +++++++++++++++++++ crates/clawhdf5/src/edit/btree2.rs | 26 +++++++++ crates/clawhdf5/src/edit/fheap.rs | 9 ++++ 3 files changed, 89 insertions(+) diff --git a/crates/clawhdf5-tools/tests/edit_coverage_interop.rs b/crates/clawhdf5-tools/tests/edit_coverage_interop.rs index 332dffa..839e32c 100644 --- a/crates/clawhdf5-tools/tests/edit_coverage_interop.rs +++ b/crates/clawhdf5-tools/tests/edit_coverage_interop.rs @@ -1396,3 +1396,57 @@ fn freed_space_is_reused_within_a_session() { ); assert!(fresh.1 > fresh.0, "without reuse the file grows"); } + +/// Replacing the last huge attribute (larger than the heap's managed +/// limit) of an object with a small one deletes the heap's huge-object +/// B-tree, as libhdf5 does when it closes the heap (`H5HF__huge_term`). A +/// heap left with an empty huge-object B-tree made read-only libhdf5 fail +/// to list the attributes ("no write intent on file"). +#[test] +fn last_huge_attribute_replaced() { + if !tools_ok() { + return; + } + let dir = tmpdir(); + let a = dir.path().join("huge_h5py.h5"); + let b = dir.path().join("huge_edit.h5"); + for p in [&a, &b] { + py(&format!( + "import h5py, numpy as np\n\ + with h5py.File({p:?}, 'w', libver='v110') as f:\n\ + \x20 g = f.create_group('g')\n\ + \x20 for i in range(10): g.attrs.create(f'k{{i}}', np.array([i], dtype=' = (0..10i64) + .map(|i| ("g".to_string(), format!("k{i}"), AV::Ints(vec![i]))) + .collect(); + want.push(("g".into(), "big".into(), AV::Ints(vec![1, 2]))); + check_attr_values(&b, &want); + let info = |p: &Path| { + let s = dense_info(p, "g"); + s[..s.find(" fs ").or(s.find(" no-fs")).unwrap()].to_string() + }; + assert_eq!(info(&b), info(&a), "heap after the replacement"); + // A huge attribute again starts the huge-object B-tree over. + let mut ed = FileEditor::open(&b).unwrap(); + ed.set_attr("g", "big2", &clawhdf5::AttrValue::String("x".repeat(7000))) + .unwrap(); + drop(ed); + want.push(("g".into(), "big2".into(), AV::Str("x".repeat(7000)))); + check_tools(&b, true); + check_attr_values(&b, &want); +} diff --git a/crates/clawhdf5/src/edit/btree2.rs b/crates/clawhdf5/src/edit/btree2.rs index 5761e1e..71bd9b2 100644 --- a/crates/clawhdf5/src/edit/btree2.rs +++ b/crates/clawhdf5/src/edit/btree2.rs @@ -384,6 +384,32 @@ impl Bt2 { } } + /// Delete the whole tree (`H5B2_delete`): every node and the header go + /// to the image's free list. + pub(crate) fn delete(mut self, img: &mut Image<'_>) -> Result<(), Error> { + if self.root.addr != undef(img.os) && self.root.nrec > 0 { + let mut level = vec![self.root]; + let mut depth = self.depth; + loop { + let mut next = Vec::new(); + for p in level { + self.load(img, p, depth)?; + if depth > 0 { + next.extend(self.peek(p.addr).ptrs.iter().copied()); + } + img.free(p.addr, u64::from(self.node_size)); + } + if depth == 0 { + break; + } + depth -= 1; + level = next; + } + } + img.free(self.addr, Self::header_len(img.os, img.ls) as u64); + Ok(()) + } + /// Insert `rec`, or replace the record `cmp` matches (`H5B2_update`). pub(crate) fn update( &mut self, diff --git a/crates/clawhdf5/src/edit/fheap.rs b/crates/clawhdf5/src/edit/fheap.rs index 2fab657..03e6b1e 100644 --- a/crates/clawhdf5/src/edit/fheap.rs +++ b/crates/clawhdf5/src/edit/fheap.rs @@ -948,6 +948,15 @@ impl Heap { img.free(get_uint(&rec, os), len); self.huge_size = self.huge_size.saturating_sub(len); self.huge_nobjs = self.huge_nobjs.saturating_sub(1); + // H5HF__huge_term: with no huge object left, the huge-object + // B-tree is deleted and IDs start over (libhdf5 does it when + // it closes the heap, and a read-only libhdf5 fails to). + if self.huge_nobjs == 0 { + t.delete(img)?; + self.huge_bt2 = undef(os); + self.next_huge_id = 0; + self.flags &= !0x01; + } Ok(()) } _ => Err(unsupported("tiny objects")),