use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct StripeRecord {
pub members: Vec<[u8; 32]>,
pub k: u8,
}
#[derive(Default, Debug)]
pub struct StripeMembershipIndex {
by_chunk: HashMap<[u8; 32], Vec<StripeRecord>>,
fingerprints: std::collections::HashSet<[u8; 32]>,
registered_count: u64,
}
impl StripeMembershipIndex {
pub fn new() -> Self {
Self::default()
}
pub fn register_stripe(&mut self, members: Vec<[u8; 32]>, k: u8) {
if members.is_empty() || k == 0 {
return; }
let mut hasher = blake3::Hasher::new();
for h in &members {
hasher.update(h);
}
hasher.update(&[k]);
let fingerprint: [u8; 32] = hasher.finalize().into();
if !self.fingerprints.insert(fingerprint) {
return; }
let record = StripeRecord {
members: members.clone(),
k,
};
for hash in &members {
self.by_chunk.entry(*hash).or_default().push(record.clone());
}
self.registered_count = self.registered_count.saturating_add(1);
}
pub fn should_pin_against_gc<F>(&self, hash: &[u8; 32], mut is_present: F) -> bool
where
F: FnMut(&[u8; 32]) -> bool,
{
let Some(stripes) = self.by_chunk.get(hash) else {
return false;
};
for stripe in stripes {
let present_count = stripe.members.iter().filter(|h| is_present(h)).count();
if present_count < stripe.k as usize {
return true;
}
}
false
}
pub fn registered_count(&self) -> u64 {
self.registered_count
}
pub fn tracked_chunk_count(&self) -> usize {
self.by_chunk.len()
}
#[cfg(test)]
pub fn clear_for_tests(&mut self) {
self.by_chunk.clear();
self.fingerprints.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn h(byte: u8) -> [u8; 32] {
let mut a = [0u8; 32];
a[0] = byte;
a
}
#[test]
fn no_pin_for_unregistered_chunk() {
let idx = StripeMembershipIndex::new();
assert!(!idx.should_pin_against_gc(&h(1), |_| true));
}
#[test]
fn no_pin_for_healthy_stripe() {
let mut idx = StripeMembershipIndex::new();
let members = vec![h(1), h(2), h(3), h(4)]; idx.register_stripe(members.clone(), 2);
for hash in &members {
assert!(!idx.should_pin_against_gc(hash, |_| true));
}
}
#[test]
fn pin_every_member_of_degraded_stripe() {
let mut idx = StripeMembershipIndex::new();
let members = vec![h(1), h(2), h(3), h(4)]; idx.register_stripe(members.clone(), 2);
let presence = |hash: &[u8; 32]| *hash == h(1);
for hash in &members {
assert!(
idx.should_pin_against_gc(hash, presence),
"every member of a degraded stripe must pin"
);
}
}
#[test]
fn no_pin_when_exactly_k_present() {
let mut idx = StripeMembershipIndex::new();
let members = vec![h(1), h(2), h(3), h(4)]; idx.register_stripe(members.clone(), 2);
let presence = |hash: &[u8; 32]| matches!(hash[0], 1 | 2);
for hash in &members {
assert!(!idx.should_pin_against_gc(hash, presence));
}
}
#[test]
fn pin_if_any_stripe_member_is_degraded() {
let mut idx = StripeMembershipIndex::new();
idx.register_stripe(vec![h(0xAA), h(2), h(3), h(4)], 2);
idx.register_stripe(vec![h(0xAA), h(5), h(6), h(7)], 2);
let presence = |hash: &[u8; 32]| matches!(hash[0], 0xAA | 2 | 3 | 4);
assert!(
idx.should_pin_against_gc(&h(0xAA), presence),
"chunk in any degraded stripe must pin"
);
}
#[test]
fn register_count_and_degenerate_no_ops() {
let mut idx = StripeMembershipIndex::new();
idx.register_stripe(vec![h(1), h(2)], 1);
assert_eq!(idx.registered_count(), 1);
idx.register_stripe(vec![], 1);
idx.register_stripe(vec![h(1)], 0);
assert_eq!(idx.registered_count(), 1, "degenerate registers are no-ops");
}
#[test]
fn dedup_repeated_registration_of_same_stripe() {
let mut idx = StripeMembershipIndex::new();
let members = vec![h(1), h(2), h(3), h(4)];
idx.register_stripe(members.clone(), 2);
idx.register_stripe(members.clone(), 2);
idx.register_stripe(members.clone(), 2);
assert_eq!(idx.registered_count(), 1);
let stripes = idx.by_chunk.get(&h(1)).unwrap();
assert_eq!(stripes.len(), 1);
}
#[test]
fn different_k_for_same_members_are_distinct() {
let mut idx = StripeMembershipIndex::new();
let members = vec![h(1), h(2), h(3), h(4)];
idx.register_stripe(members.clone(), 2);
idx.register_stripe(members.clone(), 3);
assert_eq!(idx.registered_count(), 2);
}
}