use std::collections::BTreeMap;
use sim_kernel::{Expr, Symbol};
use crate::{ConsentReceipt, FrameClock};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StoreKey(Symbol);
impl StoreKey {
pub fn new(symbol: Symbol) -> Self {
Self(symbol)
}
pub fn named(name: impl Into<String>) -> Self {
Self(Symbol::qualified("device/store", name.into()))
}
pub fn as_symbol(&self) -> &Symbol {
&self.0
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct StoredSample {
key: StoreKey,
receipt_seq: u64,
tick: u64,
content_refs: Vec<StoreKey>,
value: Expr,
}
impl StoredSample {
pub fn new(
key: StoreKey,
receipt_seq: u64,
tick: u64,
content_refs: Vec<StoreKey>,
value: Expr,
) -> Self {
Self {
key,
receipt_seq,
tick,
content_refs,
value,
}
}
pub fn key(&self) -> &StoreKey {
&self.key
}
pub fn receipt_seq(&self) -> u64 {
self.receipt_seq
}
pub fn tick(&self) -> u64 {
self.tick
}
pub fn content_refs(&self) -> &[StoreKey] {
&self.content_refs
}
pub fn value(&self) -> &Expr {
&self.value
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DeviceSampleStore {
samples: BTreeMap<StoreKey, StoredSample>,
content: BTreeMap<StoreKey, Expr>,
}
impl DeviceSampleStore {
pub fn new() -> Self {
Self::default()
}
pub fn insert_content(&mut self, key: StoreKey, value: Expr) {
self.content.insert(key, value);
}
pub fn insert_sample(&mut self, sample: StoredSample) {
self.samples.insert(sample.key.clone(), sample);
}
pub fn contains_sample(&self, key: &StoreKey) -> bool {
self.samples.contains_key(key)
}
pub fn contains_content(&self, key: &StoreKey) -> bool {
self.content.contains_key(key)
}
pub fn sample_len(&self) -> usize {
self.samples.len()
}
pub fn sample(&self, key: &StoreKey) -> Option<&StoredSample> {
self.samples.get(key)
}
pub fn content_len(&self) -> usize {
self.content.len()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Evicted {
pub key: StoreKey,
pub reason: Symbol,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrivacyMode {
Retain,
Redact,
Delete,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReaperDirective {
pub mode: PrivacyMode,
pub redact: Vec<Symbol>,
}
impl ReaperDirective {
pub fn from_receipt(receipt: &ConsentReceipt) -> Self {
let mode = if receipt.redact.is_empty() {
PrivacyMode::Retain
} else {
PrivacyMode::Redact
};
Self {
mode,
redact: receipt.redact.clone(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RetentionReaper;
impl RetentionReaper {
pub fn new() -> Self {
Self
}
pub fn sweep(
&self,
store: &mut DeviceSampleStore,
receipts: &[ConsentReceipt],
now: FrameClock,
) -> Vec<Evicted> {
let receipt_by_seq: BTreeMap<u64, &ConsentReceipt> = receipts
.iter()
.map(|receipt| (receipt.seq, receipt))
.collect();
for sample in store.samples.values_mut() {
if let Some(receipt) = receipt_by_seq.get(&sample.receipt_seq)
&& now.elapsed_ms_since(sample.tick) <= receipt.retain_ms
{
apply_redaction(&mut sample.value, &receipt.redact);
}
}
let expired: Vec<StoreKey> = store
.samples
.values()
.filter(|sample| {
receipt_by_seq
.get(&sample.receipt_seq)
.is_none_or(|receipt| now.elapsed_ms_since(sample.tick) > receipt.retain_ms)
})
.map(|sample| sample.key.clone())
.collect();
let mut evicted = Vec::new();
for key in expired {
if let Some(sample) = store.samples.remove(&key) {
evicted.push(Evicted {
key,
reason: retention_reason(),
});
for content_key in sample.content_refs {
if store.content.remove(&content_key).is_some() {
evicted.push(Evicted {
key: content_key,
reason: retention_reason(),
});
}
}
}
}
evicted
}
}
pub fn retention_reason() -> Symbol {
Symbol::qualified("device/reaper", "retention")
}
fn redacted_marker() -> Expr {
Expr::Symbol(Symbol::qualified("device/reaper", "redacted"))
}
fn apply_redaction(value: &mut Expr, redact: &[Symbol]) {
if redact.is_empty() {
return;
}
let Expr::Map(entries) = value else {
return;
};
for (key, field_value) in entries {
if let Expr::Symbol(field) = key
&& redact
.iter()
.any(|directive| redacts_field(directive, field))
{
*field_value = redacted_marker();
}
}
}
fn redacts_field(directive: &Symbol, field: &Symbol) -> bool {
directive == field
|| directive.name == field.name
|| directive.as_qualified_str() == field.as_qualified_str()
}