speccoder_runtime/extensions/
trust.rs1use 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#[must_use]
16pub fn key(name: &str) -> String {
17 format!("extension:{name}")
18}
19
20#[must_use]
22pub fn state(dir: &Path, e: &Loaded) -> TrustState {
23 store::state(dir, &key(&e.name), &e.digest)
24}
25
26#[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
59pub 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
71pub 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
87pub 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}