use std::collections::BTreeMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use super::{Digested, TrustState};
const FILE: &str = "trust.json";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Trust {
pub digest: String,
pub by: String,
pub via: String,
pub at: String,
pub definition: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Entry {
pub current: Trust,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub previous: Option<Trust>,
}
#[must_use]
pub fn read(dir: &Path) -> BTreeMap<String, Entry> {
std::fs::read_to_string(dir.join(FILE))
.ok()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or_default()
}
#[must_use]
pub fn state(dir: &Path, name: &str, digest: &str) -> TrustState {
match read(dir).get(name) {
None => TrustState::Untrusted,
Some(e) if e.current.digest == digest => TrustState::Trusted,
Some(_) => TrustState::Changed,
}
}
#[must_use]
pub fn last_trusted(dir: &Path, name: &str) -> Option<Trust> {
read(dir).remove(name).map(|e| e.current)
}
pub fn record(dir: &Path, hook: &Digested, by: &str, via: &str) {
let mut all = read(dir);
let now = Trust {
digest: hook.digest.clone(),
by: by.to_owned(),
via: via.to_owned(),
at: crate::attest::now_rfc3339(),
definition: hook.def.definition.clone(),
};
let previous = all.remove(&hook.def.name).and_then(|e| {
if e.current.digest == now.digest {
e.previous
} else {
Some(e.current)
}
});
all.insert(
hook.def.name.clone(),
Entry {
current: now,
previous,
},
);
super::write_json(dir, FILE, &all);
}
pub fn parse_flag(value: &str) -> Result<(String, String), String> {
match value.split_once('=') {
Some((n, d)) if !n.is_empty() && d.starts_with("sha256:") => {
Ok((n.to_owned(), d.to_owned()))
}
_ => Err(format!(
"--trust-hook {value}: give <name>=sha256:<digest>, as `speccoder hooks` prints it"
)),
}
}
pub fn apply_flags(
dir: &Path,
hooks: &[Digested],
flags: &[(String, String)],
by: &str,
) -> Result<(), String> {
for (name, digest) in flags {
let Some(hook) = hooks.iter().find(|h| &h.def.name == name) else {
return Err(format!("--trust-hook {name}: there is no hook `{name}`"));
};
if &hook.digest != digest {
return Err(format!(
"--trust-hook {name}: the hook is now {}, not {digest} — review it (speccoder hooks) before trusting",
hook.digest
));
}
if state(dir, name, digest) != TrustState::Trusted {
record(dir, hook, by, "ci");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn hook(dir: &Path, timeout: u64) -> Digested {
let defs = super::super::parse(&format!(
"[hooks.licence]\nat = \"tick\"\nargv = [\"true\"]\ntimeout = {timeout}\n"
))
.unwrap();
super::super::digest(&defs[0], dir)
}
#[test]
fn trust_holds_until_the_bytes_change() {
let state_dir = tempfile::tempdir().unwrap();
let repo = tempfile::tempdir().unwrap();
let h = hook(repo.path(), 5);
let d = state_dir.path();
assert_eq!(state(d, "licence", &h.digest), TrustState::Untrusted);
record(d, &h, "Ada", "terminal");
assert_eq!(state(d, "licence", &h.digest), TrustState::Trusted);
let changed = hook(repo.path(), 6);
assert_eq!(state(d, "licence", &changed.digest), TrustState::Changed);
assert_eq!(last_trusted(d, "licence").unwrap().digest, h.digest);
record(d, &changed, "Ada", "terminal");
let e = &read(d)["licence"];
assert_eq!(e.current.digest, changed.digest);
assert_eq!(e.previous.as_ref().unwrap().digest, h.digest);
}
#[test]
fn a_flag_trusts_only_the_current_digest() {
let state_dir = tempfile::tempdir().unwrap();
let repo = tempfile::tempdir().unwrap();
let h = hook(repo.path(), 5);
let d = state_dir.path();
let stale = vec![("licence".to_owned(), "sha256:00".to_owned())];
let e = apply_flags(d, std::slice::from_ref(&h), &stale, "ci").unwrap_err();
assert!(e.contains(&h.digest), "{e}");
assert_eq!(state(d, "licence", &h.digest), TrustState::Untrusted);
let good = vec![("licence".to_owned(), h.digest.clone())];
apply_flags(d, std::slice::from_ref(&h), &good, "ci").unwrap();
assert_eq!(read(d)["licence"].current.via, "ci");
assert!(apply_flags(d, &[h], &[("nope".into(), "sha256:1".into())], "ci").is_err());
assert!(parse_flag("licence").is_err());
assert_eq!(
parse_flag("licence=sha256:ab").unwrap(),
("licence".to_owned(), "sha256:ab".to_owned())
);
}
}