From 048a8b05fcae08636354607f0c00d6e95f262107 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Wed, 12 Aug 2026 14:25:39 +0900 Subject: [PATCH] Fix off-by-one that corrupts a bitmap in remove_smallest/remove_biggest When the amount to remove exactly equals the run length of the interval where removal stops, IntervalStore::remove_smallest shrank that interval to start = end + 1 and remove_biggest shrank it to end = start - 1, producing an inverted interval. run_len() then underflows on end - start (a debug-build panic, silent corruption in release): e.g. {0,1,2,4}.remove_smallest(3) returned a bitmap of length 65537 instead of {4}. Drop the exactly-consumed interval instead of shrinking it: use <= (not <) in remove_smallest and > (not >=) in remove_biggest so the boundary interval is removed by the drain. Add boundary tests for both. Fixes #359 --- roaring/src/bitmap/store/interval_store.rs | 36 ++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/roaring/src/bitmap/store/interval_store.rs b/roaring/src/bitmap/store/interval_store.rs index 2cb508fc..78e010a5 100644 --- a/roaring/src/bitmap/store/interval_store.rs +++ b/roaring/src/bitmap/store/interval_store.rs @@ -261,7 +261,10 @@ impl IntervalStore { } } if let Some(last_interval) = last_interval { - if last_interval.run_len() < amount { + // `<=`, not `<`: when the amount left equals this interval's run + // length the whole interval is consumed and must be dropped, not + // shrunk to `start = end + 1` (an inverted interval) (#359). + if last_interval.run_len() <= amount { remove_to += 1; } else { last_interval.start += amount as u16; @@ -285,7 +288,11 @@ impl IntervalStore { } } if let Some(last_interval) = last_interval { - if last_interval.run_len() >= amount { + // `>`, not `>=`: when the amount left equals this interval's run + // length the whole interval is consumed and is removed by the + // `drain` below; shrinking it would set `end = start - 1` (an + // inverted interval) (#359). + if last_interval.run_len() > amount { remove_to += 1; last_interval.end -= amount as u16; } @@ -1657,6 +1664,31 @@ mod tests { assert_eq!(interval_store, IntervalStore(alloc::vec![Interval::new_unchecked(1, 5800),])); } + #[test] + fn remove_smallest_exact_interval_boundary() { + // #359: removing exactly the run length of the first interval must drop + // it entirely, not shrink it past its end into an inverted interval. + let mut interval_store = IntervalStore(alloc::vec![ + Interval { start: 0, end: 2 }, + Interval { start: 4, end: 4 }, + ]); + interval_store.remove_smallest(3); // == run_len of [0, 2] + assert_eq!(interval_store, IntervalStore(alloc::vec![Interval::new_unchecked(4, 4)])); + } + + #[test] + fn remove_biggest_exact_interval_boundary() { + // #359: removing exactly the run length of the last interval must drop + // it entirely, not shrink its end below its start into an inverted + // interval. + let mut interval_store = IntervalStore(alloc::vec![ + Interval { start: 2, end: 2 }, + Interval { start: 4, end: 8 }, + ]); + interval_store.remove_biggest(5); // == run_len of [4, 8] + assert_eq!(interval_store, IntervalStore(alloc::vec![Interval::new_unchecked(2, 2)])); + } + #[test] fn contains_index_1() { let interval_store = IntervalStore(alloc::vec![