use rand::RngExt;
use super::storage::StatusListState;
pub fn allocate(state: &mut StatusListState) -> Option<u32> {
let available: Vec<usize> = (0..state.capacity)
.filter(|&i| !state.assigned[i])
.collect();
if available.is_empty() {
return None;
}
let mut rng = rand::rng();
let pick = rng.random_range(0..available.len());
let index = available[pick];
state.assigned[index] = true;
Some(index as u32)
}
pub fn flip(state: &mut StatusListState, index: u32, revoked: bool) -> Result<(), &'static str> {
let i = index as usize;
if i >= state.capacity {
return Err("index out of bounds for status list");
}
let byte = i / 8;
let bit = 7 - (i % 8);
if revoked {
state.bits[byte] |= 1 << bit;
} else {
state.bits[byte] &= !(1 << bit);
}
Ok(())
}
pub fn add_initial_decoys(state: &mut StatusListState, count: usize) {
let mut rng = rand::rng();
let mut added = 0_usize;
let mut attempts = 0_usize;
let max_attempts = count.saturating_mul(8).max(1024);
while added < count && attempts < max_attempts {
attempts += 1;
let idx = rng.random_range(0..state.capacity);
if state.assigned[idx] {
continue;
}
let byte = idx / 8;
let bit = 7 - (idx % 8);
if state.bits[byte] & (1 << bit) != 0 {
continue;
}
state.bits[byte] |= 1 << bit;
added += 1;
}
}
pub fn occupancy(state: &StatusListState) -> f64 {
let assigned = state.count_assigned();
let set = state.count_set();
let max = assigned.max(set);
max as f64 / state.capacity as f64
}
#[cfg(test)]
mod tests {
use super::*;
use affinidi_status_list::StatusPurpose;
fn fresh_state(capacity_hint: Option<usize>) -> StatusListState {
let mut s = StatusListState::new(
StatusPurpose::Revocation,
"https://vtc.example.com/v1/status-lists/revocation".into(),
);
if let Some(cap) = capacity_hint {
s.capacity = cap;
s.bits = vec![0u8; cap.div_ceil(8)];
s.assigned = vec![false; cap];
}
s
}
#[test]
fn allocator_exhausts_capacity_then_returns_none() {
let mut state = fresh_state(Some(16));
let mut seen = [false; 16];
for _ in 0..16 {
let idx = allocate(&mut state).expect("slot available");
assert!(!seen[idx as usize], "slot {idx} returned twice");
seen[idx as usize] = true;
}
assert!(allocate(&mut state).is_none(), "list is full");
assert!(seen.iter().all(|s| *s), "every slot must be returned once");
}
#[test]
fn allocator_never_returns_a_flipped_slot() {
let mut state = fresh_state(Some(64));
let mut flipped = Vec::new();
for _ in 0..10 {
let idx = allocate(&mut state).unwrap();
flip(&mut state, idx, true).unwrap();
flipped.push(idx);
}
while let Some(idx) = allocate(&mut state) {
assert!(
!flipped.contains(&idx),
"allocator returned previously-flipped slot {idx}"
);
}
for idx in flipped {
assert!(state.is_set(idx as usize));
}
}
#[test]
fn flip_back_to_zero_keeps_slot_assigned() {
let mut state = fresh_state(Some(16));
let idx = allocate(&mut state).unwrap();
flip(&mut state, idx, true).unwrap();
assert!(state.is_set(idx as usize));
flip(&mut state, idx, false).unwrap();
assert!(!state.is_set(idx as usize));
assert!(state.assigned[idx as usize], "slot stays assigned");
}
#[test]
fn flip_out_of_bounds_returns_err() {
let mut state = fresh_state(Some(16));
let err = flip(&mut state, 99, true).expect_err("oob must fail");
assert!(err.contains("out of bounds"));
}
#[test]
fn occupancy_crosses_threshold_at_75_percent() {
let mut state = fresh_state(Some(100));
assert!(occupancy(&state) < 0.75);
for _ in 0..75 {
allocate(&mut state).unwrap();
}
assert!(
occupancy(&state) >= 0.75,
"expected occupancy >= 0.75, got {}",
occupancy(&state)
);
}
#[test]
fn add_initial_decoys_only_touches_unassigned_slots() {
let mut state = fresh_state(Some(64));
let owned = allocate(&mut state).unwrap();
add_initial_decoys(&mut state, 30);
if !state.assigned.iter().filter(|a| **a).count() == 1 {
}
assert!(
!state.is_set(owned as usize),
"decoy must not flip an assigned slot"
);
}
}