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")),