use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use rusqlite::Connection;
use crate::governance::rules_store::Rule;
#[derive(Debug)]
pub enum RuleCacheError {
Load(anyhow::Error),
Poisoned,
}
impl std::fmt::Display for RuleCacheError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Load(e) => write!(f, "rule_cache load failed: {e:#}"),
Self::Poisoned => write!(f, "rule_cache: RwLock poisoned"),
}
}
}
impl std::error::Error for RuleCacheError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Load(e) => Some(e.as_ref()),
Self::Poisoned => None,
}
}
}
impl From<anyhow::Error> for RuleCacheError {
fn from(e: anyhow::Error) -> Self {
Self::Load(e)
}
}
impl RuleCacheError {
#[must_use]
pub fn is_poisoned(&self) -> bool {
matches!(self, Self::Poisoned)
}
}
#[derive(Debug, Default)]
pub struct RuleCache {
by_kind: RwLock<HashMap<String, Arc<Vec<Rule>>>>,
}
impl RuleCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn get_or_load(
&self,
conn: &Connection,
kind: &str,
) -> std::result::Result<Arc<Vec<Rule>>, RuleCacheError> {
if let Some(rules) = self
.by_kind
.read()
.ok()
.and_then(|guard| guard.get(kind).cloned())
{
return Ok(rules);
}
let rules = crate::governance::rules_store::list_enabled_by_kind(conn, kind)
.map_err(RuleCacheError::Load)?;
let arc = Arc::new(rules);
if let Ok(mut guard) = self.by_kind.write() {
if let Some(existing) = guard.get(kind) {
return Ok(Arc::clone(existing));
}
let entry = guard
.entry(kind.to_string())
.or_insert_with(|| Arc::clone(&arc));
return Ok(Arc::clone(entry));
}
Err(RuleCacheError::Poisoned)
}
#[inline]
pub fn invalidate(&self, kind: &str) {
if let Ok(mut guard) = self.by_kind.write() {
guard.remove(kind);
}
}
#[inline]
pub fn invalidate_all(&self) {
if let Ok(mut guard) = self.by_kind.write() {
guard.clear();
}
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.by_kind
.read()
.map(|guard| guard.len())
.unwrap_or_default()
}
#[must_use]
pub fn len_checked(&self) -> Option<usize> {
self.by_kind.read().ok().map(|guard| guard.len())
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn is_empty_checked(&self) -> Option<bool> {
self.by_kind.read().ok().map(|guard| guard.is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::governance::rules_store::Rule;
fn sample_rule(id: &str, kind: &str) -> Rule {
Rule {
id: id.to_string(),
kind: kind.to_string(),
matcher: "{}".to_string(),
severity: "log".to_string(),
reason: "test".to_string(),
namespace: String::new(),
created_by: "test".to_string(),
created_at: 0,
enabled: true,
signature: None,
attest_level: "unsigned".to_string(),
}
}
#[test]
fn invalidate_all_clears_every_entry_991() {
let cache = RuleCache::new();
{
let mut g = cache.by_kind.write().unwrap();
g.insert(
"bash".to_string(),
Arc::new(vec![sample_rule("r1", "bash")]),
);
g.insert(
"filesystem_write".to_string(),
Arc::new(vec![sample_rule("r2", "filesystem_write")]),
);
}
assert_eq!(cache.len(), 2);
cache.invalidate_all();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn invalidate_specific_kind_keeps_others_991() {
let cache = RuleCache::new();
{
let mut g = cache.by_kind.write().unwrap();
g.insert(
"bash".to_string(),
Arc::new(vec![sample_rule("r1", "bash")]),
);
g.insert(
"filesystem_write".to_string(),
Arc::new(vec![sample_rule("r2", "filesystem_write")]),
);
}
cache.invalidate("bash");
assert_eq!(cache.len(), 1);
let remaining = cache.by_kind.read().unwrap();
assert!(remaining.contains_key("filesystem_write"));
assert!(!remaining.contains_key("bash"));
}
#[test]
fn cross_instance_isolation_no_poisoning_991() {
let cache_a = RuleCache::new();
let cache_b = RuleCache::new();
{
let mut g = cache_a.by_kind.write().unwrap();
g.insert(
"filesystem_write".to_string(),
Arc::new(vec![sample_rule("peer-a-r", "filesystem_write")]),
);
}
assert_eq!(cache_a.len(), 1);
assert_eq!(cache_b.len(), 0);
}
#[test]
fn rule_cache_error_implements_std_error_1020() {
let load_err: RuleCacheError = anyhow::anyhow!("synthetic rusqlite failure").into();
assert!(matches!(load_err, RuleCacheError::Load(_)));
assert!(!load_err.is_poisoned());
let display = format!("{load_err}");
assert!(
display.contains("rule_cache load failed")
&& display.contains("synthetic rusqlite failure"),
"#1020: Load Display MUST surface the wrapped anyhow chain; got {display}"
);
let poison_err = RuleCacheError::Poisoned;
assert!(poison_err.is_poisoned());
assert!(format!("{poison_err}").contains("RwLock poisoned"));
let upcast: anyhow::Error = poison_err.into();
assert!(format!("{upcast:#}").contains("RwLock poisoned"));
}
#[test]
fn dropped_instance_drops_entries_991() {
let weak;
{
let cache = RuleCache::new();
let entry = Arc::new(vec![sample_rule("r1", "bash")]);
weak = Arc::downgrade(&entry);
cache
.by_kind
.write()
.unwrap()
.insert("bash".to_string(), entry);
assert!(weak.upgrade().is_some(), "entry alive while cache alive");
}
assert!(
weak.upgrade().is_none(),
"cache drop must release Arc<Vec<Rule>> entries"
);
}
}