From ed29a27e598a1b1c816ea811e8d2d8dffaee9b7b Mon Sep 17 00:00:00 2001 From: Igor Malovitsa Date: Thu, 17 Sep 2026 05:03:56 +0000 Subject: [PATCH 1/2] Fix exclusive zippers rooted at an emptied link prepare_cf returned an existing link even when it was the empty sentinel, left by remove_branches, take_map or grafting nothing. A write zipper rooted there couldn't move or read. Give it a fresh node, as for a missing link. --- src/dense_byte_node.rs | 13 +++++-------- src/zipper_head.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index ab6bf2db..9cf736ae 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -636,14 +636,11 @@ impl CellByteNode { } let cf = self.get_mut(k).unwrap(); let (rec, val) = cf.both_mut_refs(); - let rec = match rec { - Some(rec) => rec, - None => { - *rec = Some(TrieNodeODRc::new_allocated_in(0, 0, alloc)); - rec.as_mut().unwrap() - } - }; - (rec, val) + //A zipper can't be rooted at the empty sentinel, so an emptied link gets a real node too + if rec.as_ref().map_or(true, |rec| rec.is_empty()) { + *rec = Some(TrieNodeODRc::new_allocated_in(0, 0, alloc)); + } + (rec.as_mut().unwrap(), val) } } diff --git a/src/zipper_head.rs b/src/zipper_head.rs index 20357a74..a2d46bad 100644 --- a/src/zipper_head.rs +++ b/src/zipper_head.rs @@ -1637,4 +1637,34 @@ mod tests { } } } + + /// An exclusive zipper rooted at a link emptied by `remove_branches`, `take_map` or a graft of nothing + #[test] + fn exclusive_zipper_at_emptied_link() { + let preps: [fn(&mut WriteZipperUntracked); 4] = [ + |z| { z.remove_branches(false); }, + |z| { z.take_map(false); }, + |z| { let e = PathMap::::new(); z.graft(&e.read_zipper()); }, + |z| { let e = PathMap::::new(); z.restrict(&e.read_zipper()); }, + ]; + for (i, prep) in preps.iter().enumerate() { + let mut map = PathMap::::new(); + for p in [&[0u8, 0, 1][..], &[0, 0, 1, 2], &[0]] { map.set_val_at(p, 1); } + { + let mut wz = map.write_zipper_at_path(&[0u8, 0]); + prep(&mut wz); + let zh = wz.zipper_head(); + let mut w = zh.write_zipper_at_exclusive_path(&[]).unwrap(); + assert!(!w.descend_to_existing_byte(1), "prep {i}"); + w.descend_to(&[7u8, 7]); + assert!(!w.path_exists(), "prep {i}"); + assert_eq!(w.ascend_until(), 2, "prep {i}"); + w.descend_to(&[7u8, 7]); + w.set_val(5); + } + assert_eq!(map.get(&[0u8, 0, 7, 7]), Some(&5), "prep {i}"); + assert_eq!(map.get(&[0u8]), Some(&1), "prep {i}"); + assert_eq!(map.val_count(), 2, "prep {i}"); + } + } } From 889382076fed16210e873dfbfb2560419e7cadd0 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Thu, 1 Oct 2026 22:07:05 -0600 Subject: [PATCH 2/2] Adding additional test to be sure converted zipper-root-node gets pruned appropriately --- src/zipper_head.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/zipper_head.rs b/src/zipper_head.rs index a2d46bad..a7fb8624 100644 --- a/src/zipper_head.rs +++ b/src/zipper_head.rs @@ -1503,6 +1503,22 @@ mod tests { drop(rz); } + #[test] + fn cleanup_write_zipper_at_emptied_link() { + let mut map = PathMap::::new(); + map.set_val_at(&[0u8], 1); + map.set_val_at(&[0u8, 0, 1], 2); + map.write_zipper_at_path(&[0u8, 0]).remove_branches(false); + assert!(map.read_zipper_at_path(&[0u8, 0]).path_exists()); + + let zh = map.zipper_head(); + let wz = zh.write_zipper_at_exclusive_path(&[0u8, 0]).unwrap(); + zh.cleanup_write_zipper(wz); + + assert!(!zh.read_zipper_at_path(&[0u8, 0]).unwrap().path_exists()); + assert_eq!(zh.read_zipper_at_path(&[0u8]).unwrap().val(), Some(&1)); + } + /// A ZipperHead turns the parent node into a CellByteNode; joining a list node with it must not /// treat the cell node as a DenseByteNode #[test]