use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
pub fn policy_hash(policy_bytes: &[u8]) -> String {
hex::encode(Sha256::digest(policy_bytes))
}
pub const BUILTIN: &str = "builtin";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Mark {
pub ts: u64,
pub policy_hash: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PolicyDoc {
pub hash: String,
pub body: Vec<u8>,
pub source: String,
}
#[derive(Default)]
pub struct PolicySet {
docs: BTreeMap<String, PolicyDoc>,
}
impl PolicySet {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, body: Vec<u8>, source: &str) -> String {
let hash = policy_hash(&body);
self.docs.insert(
hash.clone(),
PolicyDoc {
hash: hash.clone(),
body,
source: source.to_string(),
},
);
hash
}
pub fn resolve(&self, hash: &str) -> Option<&PolicyDoc> {
self.docs.get(hash)
}
pub fn hashes(&self) -> Vec<&str> {
self.docs.keys().map(|s| s.as_str()).collect()
}
}
#[derive(Debug, Default, Clone)]
pub struct WinterCount {
marks: Vec<Mark>,
}
impl WinterCount {
pub fn new() -> Self {
Self::default()
}
pub fn mark(&mut self, ts: u64, policy_hash: impl Into<String>) {
self.marks.push(Mark {
ts,
policy_hash: policy_hash.into(),
label: None,
});
}
pub fn mark_labeled(&mut self, ts: u64, policy_hash: impl Into<String>, label: &str) {
self.marks.push(Mark {
ts,
policy_hash: policy_hash.into(),
label: Some(label.to_string()),
});
}
pub fn marks(&self) -> &[Mark] {
&self.marks
}
pub fn policy_at(&self, ts: u64) -> Option<&str> {
self.marks
.iter()
.rev()
.find(|m| m.ts <= ts)
.map(|m| m.policy_hash.as_str())
}
pub fn transitions(&self) -> Vec<(u64, Option<String>, String)> {
let mut out = Vec::new();
let mut prev: Option<&str> = None;
for m in &self.marks {
if prev != Some(m.policy_hash.as_str()) {
out.push((m.ts, prev.map(String::from), m.policy_hash.clone()));
prev = Some(&m.policy_hash);
}
}
out
}
pub fn governed_range(&self, hash: &str) -> Option<(u64, u64)> {
let mut first = None;
let mut last = None;
for m in &self.marks {
if m.policy_hash == hash {
if first.is_none() {
first = Some(m.ts);
}
last = Some(m.ts);
}
}
first.zip(last)
}
pub fn census(&self) -> BTreeMap<String, usize> {
let mut c = BTreeMap::new();
for m in &self.marks {
*c.entry(m.policy_hash.clone()).or_insert(0) += 1;
}
c
}
pub fn foreign_marks(&self, known: &PolicySet, accept_builtin: bool) -> Vec<&Mark> {
self.marks
.iter()
.filter(|m| {
if accept_builtin && m.policy_hash == BUILTIN {
return false;
}
known.resolve(&m.policy_hash).is_none()
})
.collect()
}
}
pub fn from_ledger(lines: impl Iterator<Item = String>) -> WinterCount {
let mut wc = WinterCount::new();
for (i, line) in lines.enumerate() {
let Ok(v) = serde_json::from_str::<Value>(&line) else {
continue;
};
let hash = v["data"]["policy_hash"]
.as_str()
.or_else(|| v["policy_hash"].as_str());
let Some(hash) = hash else { continue };
let ts = v["ts"]
.as_u64()
.or_else(|| v["timestamp"].as_u64())
.or_else(|| v["data"]["ts"].as_u64())
.unwrap_or(i as u64);
let label = v["type"]
.as_str()
.or_else(|| v["data"]["type"].as_str());
match label {
Some(l) => wc.mark_labeled(ts, hash, l),
None => wc.mark(ts, hash),
}
}
wc
}