use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerTrustEntry {
pub trust: aegis_security::TrustLevel,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PeerTrustDb {
#[serde(default)]
pub peers: HashMap<String, PeerTrustEntry>,
}
impl PeerTrustDb {
pub fn default_path() -> PathBuf {
crate::config::config_dir().join("peers_trust.toml")
}
pub fn load(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::default());
}
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
toml::from_str::<Self>(&text).with_context(|| format!("parsing {}", path.display()))
}
pub fn save(&self, path: &Path) -> Result<()> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)
.with_context(|| format!("creating dir {}", dir.display()))?;
}
let text = toml::to_string_pretty(self).context("serializing peers_trust.toml")?;
let tmp = path.with_extension("toml.tmp");
std::fs::write(&tmp, text.as_bytes())
.with_context(|| format!("writing tmp {}", tmp.display()))?;
std::fs::rename(&tmp, path)
.with_context(|| format!("renaming into {}", path.display()))?;
Ok(())
}
pub fn set(
&mut self,
agent_id: &str,
trust: aegis_security::TrustLevel,
note: Option<String>,
) -> bool {
let existed = self.peers.contains_key(agent_id);
let now = chrono::Utc::now().to_rfc3339();
self.peers.insert(
agent_id.to_string(),
PeerTrustEntry {
trust,
note,
updated_at: Some(now),
},
);
!existed
}
pub fn remove(&mut self, agent_id: &str) -> bool {
self.peers.remove(agent_id).is_some()
}
pub fn get(&self, agent_id: &str) -> Option<aegis_security::TrustLevel> {
self.peers.get(agent_id).map(|e| e.trust)
}
pub fn list_sorted(&self) -> Vec<(&String, &PeerTrustEntry)> {
let mut v: Vec<_> = self.peers.iter().collect();
v.sort_by(|a, b| a.0.cmp(b.0));
v
}
}
pub fn effective_trust(
db: &PeerTrustDb,
config_peers: &[crate::config::PeerConfig],
agent_id: &str,
) -> aegis_security::TrustLevel {
if let Some(t) = db.get(agent_id) {
return t;
}
crate::config::peer_trust_level(config_peers, agent_id)
}
#[cfg(test)]
mod tests {
use super::*;
use aegis_security::TrustLevel;
#[test]
fn set_and_get_roundtrip() {
let mut db = PeerTrustDb::default();
let created = db.set("foo", TrustLevel::Trusted, None);
assert!(created);
assert_eq!(db.get("foo"), Some(TrustLevel::Trusted));
let created = db.set("foo", TrustLevel::Restricted, Some("demoted".into()));
assert!(!created);
assert_eq!(db.get("foo"), Some(TrustLevel::Restricted));
}
#[test]
fn remove_returns_true_when_existed() {
let mut db = PeerTrustDb::default();
db.set("foo", TrustLevel::Standard, None);
assert!(db.remove("foo"));
assert!(!db.remove("foo"));
}
#[test]
fn save_and_load_roundtrip() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("peers_trust.toml");
let mut db = PeerTrustDb::default();
db.set("alice", TrustLevel::Trusted, None);
db.set("bob", TrustLevel::Restricted, Some("group chat".into()));
db.save(&path).expect("save");
let loaded = PeerTrustDb::load(&path).expect("load");
assert_eq!(loaded.get("alice"), Some(TrustLevel::Trusted));
assert_eq!(loaded.get("bob"), Some(TrustLevel::Restricted));
}
#[test]
fn load_missing_file_returns_empty() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("nope.toml");
let db = PeerTrustDb::load(&path).expect("load absent");
assert!(db.peers.is_empty());
}
#[test]
fn effective_trust_prefers_db() {
use crate::config::PeerConfig;
let mut db = PeerTrustDb::default();
db.set("alice", TrustLevel::Owner, None);
let peers = vec![PeerConfig {
name: "alice".into(),
trust_level: TrustLevel::Restricted, ..Default::default()
}];
assert_eq!(effective_trust(&db, &peers, "alice"), TrustLevel::Owner);
}
#[test]
fn effective_trust_falls_back_to_config() {
use crate::config::PeerConfig;
let db = PeerTrustDb::default();
let peers = vec![PeerConfig {
name: "bob".into(),
trust_level: TrustLevel::Trusted,
..Default::default()
}];
assert_eq!(effective_trust(&db, &peers, "bob"), TrustLevel::Trusted);
}
#[test]
fn effective_trust_default_is_readonly() {
let db = PeerTrustDb::default();
assert_eq!(effective_trust(&db, &[], "unknown"), TrustLevel::ReadOnly);
}
}