use roaring::RoaringBitmap;
const MAGIC: [u8; 4] = [0x53, 0x4D, 0x45, 0x4D]; const VERSION: u8 = 0x01;
#[derive(Debug, Clone)]
pub struct ScanMemo {
pub start_height: u32,
pub end_height: u32,
bitmap: RoaringBitmap,
}
impl ScanMemo {
#[must_use]
pub fn new(start_height: u32) -> Self {
Self {
start_height,
end_height: start_height,
bitmap: RoaringBitmap::new(),
}
}
pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
if bytes.len() < 13 {
return Err(crate::Error::InvalidResponse("ScanMemo too short".into()));
}
if bytes[..4] != MAGIC {
return Err(crate::Error::InvalidResponse("ScanMemo bad magic".into()));
}
if bytes[4] != VERSION {
return Err(crate::Error::InvalidResponse(format!(
"ScanMemo unsupported version {}",
bytes[4]
)));
}
let start_height = u32::from_le_bytes(bytes[5..9].try_into().unwrap());
let end_height = u32::from_le_bytes(bytes[9..13].try_into().unwrap());
let bitmap = RoaringBitmap::deserialize_from(&bytes[13..])
.map_err(|e| crate::Error::InvalidResponse(format!("ScanMemo bitmap: {e}")))?;
Ok(Self {
start_height,
end_height,
bitmap,
})
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&MAGIC);
out.push(VERSION);
out.extend_from_slice(&self.start_height.to_le_bytes());
out.extend_from_slice(&self.end_height.to_le_bytes());
self.bitmap
.serialize_into(&mut out)
.expect("in-memory write");
out
}
pub fn mark_scanned(&mut self, height: u32) {
self.bitmap.insert(height);
if height > self.end_height {
self.end_height = height;
}
}
#[must_use]
pub fn is_scanned(&self, height: u32) -> bool {
self.bitmap.contains(height)
}
#[must_use]
pub fn first_unscanned_from(&self, from: u32) -> u32 {
let mut h = from;
while self.bitmap.contains(h) {
h += 1;
}
h
}
#[must_use]
pub fn scanned_count(&self) -> u64 {
self.bitmap.len()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn progress_pct(&self) -> f64 {
let total = f64::from(self.end_height - self.start_height + 1);
if total == 0.0 {
return 100.0;
}
(self.scanned_count() as f64 / total) * 100.0
}
pub fn merge(&mut self, other: &ScanMemo) {
self.bitmap |= &other.bitmap;
if other.start_height < self.start_height {
self.start_height = other.start_height;
}
if other.end_height > self.end_height {
self.end_height = other.end_height;
}
}
#[must_use]
pub fn unscanned_ranges(&self, from: u32, to: u32) -> Vec<(u32, u32)> {
let mut ranges = Vec::new();
let mut range_start: Option<u32> = None;
for h in from..=to {
if !self.bitmap.contains(h) {
if range_start.is_none() {
range_start = Some(h);
}
} else if let Some(start) = range_start.take() {
ranges.push((start, h - 1));
}
}
if let Some(start) = range_start {
ranges.push((start, to));
}
ranges
}
}
impl Default for ScanMemo {
fn default() -> Self {
Self::new(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_empty() {
let memo = ScanMemo::new(840_000);
let bytes = memo.to_bytes();
let back = ScanMemo::from_bytes(&bytes).unwrap();
assert_eq!(back.start_height, 840_000);
assert_eq!(back.scanned_count(), 0);
}
#[test]
fn mark_and_check() {
let mut memo = ScanMemo::new(100);
memo.mark_scanned(100);
memo.mark_scanned(102);
assert!(memo.is_scanned(100));
assert!(!memo.is_scanned(101));
assert!(memo.is_scanned(102));
}
#[test]
fn resume_logic() {
let mut memo = ScanMemo::new(100);
memo.mark_scanned(100);
memo.mark_scanned(101);
memo.mark_scanned(102);
assert_eq!(memo.first_unscanned_from(100), 103);
}
#[test]
fn unscanned_ranges() {
let mut memo = ScanMemo::new(100);
memo.mark_scanned(101);
memo.mark_scanned(103);
let ranges = memo.unscanned_ranges(100, 105);
assert_eq!(ranges, vec![(100, 100), (102, 102), (104, 105)]);
}
#[test]
fn roundtrip_with_data() {
let mut memo = ScanMemo::new(840_000);
for h in 840_000..840_100 {
memo.mark_scanned(h);
}
let bytes = memo.to_bytes();
let back = ScanMemo::from_bytes(&bytes).unwrap();
assert_eq!(back.scanned_count(), 100);
for h in 840_000..840_100 {
assert!(back.is_scanned(h));
}
}
#[test]
fn merge_memos() {
let mut m1 = ScanMemo::new(100);
m1.mark_scanned(100);
m1.mark_scanned(101);
let mut m2 = ScanMemo::new(102);
m2.mark_scanned(102);
m2.mark_scanned(103);
m1.merge(&m2);
assert!(m1.is_scanned(101));
assert!(m1.is_scanned(102));
}
}