use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use super::approval::{Approval, SignedApproval, verify_approval, verify_approval_signature};
use super::persist::{read_capped, write_atomic};
use super::{LicenceError, LocalMeshId, MAX_APPROVALS, Outcome};
use crate::workload_pkg::TrustAnchors;
pub const APPROVALS_FILE: &str = "checkout_approvals.json";
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ApprovalsFile {
v: u32,
approvals: Vec<SignedApproval>,
}
pub(super) type Held = BTreeMap<String, (SignedApproval, Approval)>;
pub struct ApprovalStore {
path: PathBuf,
anchors: Arc<TrustAnchors>,
pub(super) local: LocalMeshId,
inner: Mutex<(Held, Option<String>)>,
}
impl ApprovalStore {
pub fn open(
dir: &Path,
anchors: Arc<TrustAnchors>,
local: LocalMeshId,
) -> Result<Self, LicenceError> {
let path = dir.join(APPROVALS_FILE);
let held = load(&path, &anchors)?;
Ok(Self { path, anchors, local, inner: Mutex::new((held, None)) })
}
pub fn open_or_poisoned(dir: &Path, anchors: Arc<TrustAnchors>, local: LocalMeshId) -> Self {
let path = dir.join(APPROVALS_FILE);
let (held, poison) = match load(&path, &anchors) {
Ok(h) => (h, None),
Err(e) => (Held::new(), Some(e.to_string())),
};
Self { path, anchors, local, inner: Mutex::new((held, poison)) }
}
pub(super) fn lock(&self) -> std::sync::MutexGuard<'_, (Held, Option<String>)> {
self.inner.lock().unwrap_or_else(|p| p.into_inner())
}
pub fn poisoned(&self) -> Option<String> {
self.lock().1.clone()
}
pub fn accept(&self, signed: &SignedApproval) -> Result<Outcome, LicenceError> {
let mut g = self.lock();
if let Some(p) = &g.1 {
return Err(LicenceError::Poisoned(p.clone()));
}
let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
let approval = verify_approval(signed, &self.anchors, &local)?;
let key = approval.content_key();
if g.0.contains_key(&key) {
return Ok(Outcome::Duplicate);
}
if g.0.len() >= MAX_APPROVALS {
return Err(LicenceError::Full);
}
g.0.insert(key, (signed.clone(), approval));
save(&self.path, &g.0)?;
Ok(Outcome::Applied)
}
pub fn covering(&self, cog_id: &str, version: &str, sha256: &str) -> Option<String> {
let g = self.lock();
let local = self.local.get()?.to_hex();
if g.1.is_some() {
return None;
}
g.0.iter()
.find(|(_, (_, a))| {
a.mesh_id == local && a.cog_id == cog_id && a.version == version && a.covers(sha256)
})
.map(|(k, _)| k.clone())
}
pub fn orphaned(&self) -> Vec<String> {
let g = self.lock();
let local = self.local.get().map(|m| m.to_hex());
g.0.iter()
.filter(|(_, (_, a))| Some(&a.mesh_id) != local.as_ref())
.map(|(k, _)| k.clone())
.collect()
}
pub fn len(&self) -> usize {
self.lock().0.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
fn save(path: &Path, held: &Held) -> Result<(), LicenceError> {
let file = ApprovalsFile { v: 1, approvals: held.values().map(|(s, _)| s.clone()).collect() };
let bytes = serde_json::to_vec(&file).map_err(|e| LicenceError::Persist(e.to_string()))?;
write_atomic(path, &bytes)
}
fn load(path: &Path, anchors: &TrustAnchors) -> Result<Held, LicenceError> {
let Some(bytes) = read_capped(path)? else {
return Ok(Held::new());
};
let file: ApprovalsFile =
serde_json::from_slice(&bytes).map_err(|e| LicenceError::Malformed(e.to_string()))?;
if file.v != 1 || file.approvals.len() > MAX_APPROVALS {
return Err(LicenceError::Malformed("approvals version or size".into()));
}
let mut held = Held::new();
for s in file.approvals {
let a = verify_approval_signature(&s, anchors)?;
held.insert(a.content_key(), (s, a));
}
Ok(held)
}