Skip to main content

speccoder_runtime/extensions/
trust.rs

1//! Who trusted which extension's bytes: entries `extension:<name>` in the
2//! hooks' trust record, holding the digest, the files and the
3//! contributions the person saw — so a changed extension shows what changed
4//! and what widened.
5
6use std::path::Path;
7
8use speccoder_core::extensions::{Contribution, ExtensionChanges};
9use speccoder_core::hooks::{HookFile, TrustState};
10
11use super::Loaded;
12use crate::hooks::trust as store;
13
14/// The trust record's key for `name`.
15#[must_use]
16pub fn key(name: &str) -> String {
17    format!("extension:{name}")
18}
19
20/// Whether `e` may run, as it is now.
21#[must_use]
22pub fn state(dir: &Path, e: &Loaded) -> TrustState {
23    store::state(dir, &key(&e.name), &e.digest)
24}
25
26/// What changed since `e` was last trusted; `None` when never trusted or
27/// unchanged.
28#[must_use]
29pub fn changes(dir: &Path, e: &Loaded) -> Option<ExtensionChanges> {
30    let before = store::last_trusted(dir, &key(&e.name))?;
31    if before.digest == e.digest {
32        return None;
33    }
34    let files_before: Vec<HookFile> =
35        serde_json::from_value(before.definition["files"].clone()).unwrap_or_default();
36    let seen: Vec<Contribution> =
37        serde_json::from_value(before.definition["contributions"].clone()).unwrap_or_default();
38    let mut files: Vec<String> = e
39        .files
40        .iter()
41        .filter(|f| !files_before.contains(f))
42        .map(|f| f.path.clone())
43        .chain(
44            files_before
45                .iter()
46                .filter(|b| !e.files.iter().any(|f| f.path == b.path))
47                .map(|b| format!("{} (gone)", b.path)),
48        )
49        .collect();
50    files.sort();
51    let (widened, gone) = speccoder_core::extensions::widened(&seen, &e.contributions());
52    Some(ExtensionChanges {
53        files,
54        widened,
55        gone,
56    })
57}
58
59/// Records that `by` trusted `e` as it is now, through `via`.
60pub fn record(dir: &Path, e: &Loaded, by: &str, via: &str) {
61    store::record_as(
62        dir,
63        &key(&e.name),
64        &e.digest,
65        serde_json::json!({ "files": e.files, "contributions": e.contributions() }),
66        by,
67        via,
68    );
69}
70
71/// `--trust-extension name=sha256:…`, split.
72///
73/// # Errors
74///
75/// The value is not `name=digest`.
76pub fn parse_flag(value: &str) -> Result<(String, String), String> {
77    match value.split_once('=') {
78        Some((n, d)) if !n.is_empty() && d.starts_with("sha256:") => {
79            Ok((n.to_owned(), d.to_owned()))
80        }
81        _ => Err(format!(
82            "--trust-extension {value}: give <name>=sha256:<digest>, as `speccoder extensions` prints it"
83        )),
84    }
85}
86
87/// Trusts the extensions `flags` name, each only at its current digest
88/// (`via = ci`).
89///
90/// # Errors
91///
92/// A flag names no extension, one that cannot be loaded, or a digest that is
93/// not the current one (the message names it).
94pub fn apply_flags(
95    dir: &Path,
96    all: &[Loaded],
97    flags: &[(String, String)],
98    by: &str,
99) -> Result<(), String> {
100    for (name, digest) in flags {
101        let Some(e) = all.iter().find(|e| &e.name == name) else {
102            return Err(format!(
103                "--trust-extension {name}: there is no extension `{name}`"
104            ));
105        };
106        if !e.usable() {
107            return Err(format!(
108                "--trust-extension {name}: it cannot be loaded: {}",
109                e.problems.join("; ")
110            ));
111        }
112        if &e.digest != digest {
113            return Err(format!(
114                "--trust-extension {name}: the extension is now {}, not {digest} — review it (speccoder extensions) before trusting",
115                e.digest
116            ));
117        }
118        if state(dir, e) != TrustState::Trusted {
119            record(dir, e, by, "ci");
120        }
121    }
122    Ok(())
123}