use std::{
collections::{HashMap, VecDeque},
fs::OpenOptions,
io::Write,
path::PathBuf,
};
use anyhow::{anyhow, Context, Result};
use linprov_common::fnv_hash;
use crate::allowlist::{RuleSpec, Rules};
const MAX_BLOCKS: usize = 512;
#[derive(Clone, Debug)]
pub struct BlockEvent {
pub kind: &'static str,
pub token: String,
pub target: String,
pub creator: String,
}
impl BlockEvent {
pub fn to_wire(&self) -> String {
format!(
"BLOCK\t{}\t{}\t{}\t{}",
self.token, self.kind, self.target, self.creator
)
}
}
#[derive(Default)]
pub struct BlocksTable {
map: HashMap<String, String>,
order: VecDeque<String>,
}
impl BlocksTable {
pub fn record(&mut self, rule_line: String) -> String {
let token = format!("{:08x}", fnv_hash(&rule_line) as u32);
if !self.map.contains_key(&token) {
if self.order.len() >= MAX_BLOCKS {
if let Some(old) = self.order.pop_front() {
self.map.remove(&old);
}
}
self.order.push_back(token.clone());
self.map.insert(token.clone(), rule_line);
}
token
}
fn rule_for(&self, token: &str) -> Option<&str> {
self.map.get(token).map(String::as_str)
}
}
pub struct Control {
allowlist_path: Option<PathBuf>,
transient: Vec<RuleSpec>,
pub blocks: BlocksTable,
}
impl Control {
pub fn new(allowlist_path: Option<PathBuf>) -> Self {
Self {
allowlist_path,
transient: Vec::new(),
blocks: BlocksTable::default(),
}
}
pub fn combined(&self) -> Result<Vec<RuleSpec>> {
let mut rules = match &self.allowlist_path {
Some(p) => Rules::load(p)?.rules,
None => Vec::new(),
};
rules.extend(self.transient.iter().cloned());
Ok(rules)
}
pub fn apply(&mut self, token: &str, once: bool) -> Result<String> {
let line = self
.blocks
.rule_for(token)
.ok_or_else(|| {
anyhow!("unknown token `{token}` (expired, or nothing blocked this daemon session)")
})?
.to_string();
RuleSpec::parse(&line).with_context(|| format!("re-parsing candidate rule `{line}`"))?;
if once {
if !self.transient.iter().any(|r| r.to_line() == line) {
let spec = RuleSpec::parse(&line)?;
self.transient.push(spec);
}
} else {
self.append_persistent(&line)?;
}
Ok(line)
}
fn append_persistent(&mut self, line: &str) -> Result<()> {
let path = self.allowlist_path.as_ref().ok_or_else(|| {
anyhow!("no allowlist file configured; cannot persist this rule (try --once)")
})?;
if Rules::load(path)?.rules.iter().any(|r| r.to_line() == line) {
return Ok(());
}
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.with_context(|| format!("opening `{}` to append", path.display()))?;
writeln!(f, "{line}").with_context(|| format!("appending `{line}`"))?;
f.sync_data().ok();
Ok(())
}
}