cljrs_runtime/env/vcs.rs
1//! The runtime's interface to version control.
2//!
3//! Versioned symbol resolution (`ns/name@commit`) and commit-signature
4//! verification are the only two places where the interpreter touches git.
5//! Calling `cljrs_project::vcs` directly from here would link gitoxide, rPGP,
6//! `ssh-key` — and, through gix's blocking http transport, reqwest/hyper/rustls
7//! — into *every* embedding of the interpreter, including ones that never
8//! resolve a versioned var.
9//!
10//! So the runtime talks to a [`VcsProvider`] trait object instead:
11//!
12//! * With the default `deps` feature on, [`GlobalEnv::new`] installs
13//! [`ProjectVcs`] — the `cljrs-project`-backed implementation — so behaviour
14//! is exactly what it was before the split and no downstream crate changes.
15//! * Built with `--no-default-features`, no provider is installed:
16//! [`VcsProvider::find_repo_root`] never gets asked, so a source file is
17//! treated as "not in a git repository", versioned resolution falls back to
18//! embedded (AOT) sources, and gix/pgp/ssh-key are not compiled at all.
19//!
20//! The `GlobalEnv` field holding the provider exists in both configurations,
21//! so the struct's layout does not change with the feature — an embedder that
22//! links a `deps`-less runtime alongside a `deps`-ful one is not exposed to a
23//! feature-unification surprise.
24//!
25//! [`GlobalEnv::new`]: crate::env::env::GlobalEnv::new
26
27use std::path::{Path, PathBuf};
28
29use cljrs_project::config::TrustedSigner;
30
31/// Why a commit-signature check did not succeed.
32#[derive(Debug)]
33pub enum SignatureFailure {
34 /// The commit is unsigned, its signature is invalid, or the signing key is
35 /// not in the trusted set. Surfaces to user code as
36 /// `EvalError::CommitSignatureVerificationFailed`.
37 Untrusted { commit: String, reason: String },
38 /// The check could not be carried out at all (malformed hash, unreadable
39 /// repository, …). Surfaces as a plain runtime error.
40 Error(String),
41}
42
43/// The git operations the runtime needs, as an interface.
44///
45/// Implementations must be cheap to clone-by-`Arc` and safe to call from any
46/// thread: a provider is shared by every thread evaluating in a `GlobalEnv`.
47pub trait VcsProvider: Send + Sync {
48 /// Walk upward from `start` (a file or directory) to the enclosing git
49 /// working-tree root, or `None` when `start` is not inside a repository.
50 fn find_repo_root(&self, start: &Path) -> Option<PathBuf>;
51
52 /// Read `rel_path` (relative to `repo_root`) as it existed at `commit`.
53 fn file_at_commit(
54 &self,
55 repo_root: &Path,
56 rel_path: &str,
57 commit: &str,
58 ) -> Result<String, String>;
59
60 /// Verify that `commit` in `repo_root` carries a cryptographically valid
61 /// signature made by one of the keys installed by
62 /// [`load_trusted_signers`](VcsProvider::load_trusted_signers).
63 fn verify_commit_signature(
64 &self,
65 repo_root: &Path,
66 commit: &str,
67 ) -> Result<(), SignatureFailure>;
68
69 /// Install the set of keys trusted to sign versioned dependency commits,
70 /// replacing any previously installed set. Returns the number of keys
71 /// successfully loaded; malformed or unreadable keys are warned about
72 /// rather than aborting.
73 fn load_trusted_signers(&self, signers: &[TrustedSigner]) -> usize;
74}
75
76/// The provider installed by [`GlobalEnv::new`], or `None` in builds that
77/// carry no VCS implementation.
78///
79/// [`GlobalEnv::new`]: crate::env::env::GlobalEnv::new
80pub fn default_provider() -> Option<std::sync::Arc<dyn VcsProvider>> {
81 #[cfg(all(feature = "deps", not(target_arch = "wasm32")))]
82 {
83 Some(std::sync::Arc::new(ProjectVcs::new()))
84 }
85 #[cfg(not(all(feature = "deps", not(target_arch = "wasm32"))))]
86 {
87 None
88 }
89}
90
91// ── cljrs-project-backed implementation ───────────────────────────────────────
92
93/// [`VcsProvider`] implemented on top of `cljrs_project::vcs` (gitoxide for the
94/// git side, rPGP / `ssh-key` for signatures).
95#[cfg(all(feature = "deps", not(target_arch = "wasm32")))]
96pub struct ProjectVcs {
97 /// Public keys trusted to sign versioned dependency commits, built from the
98 /// `:trusted-signers` config. Empty until `load_trusted_signers` runs, in
99 /// which case every signature check fails as untrusted.
100 trusted: std::sync::RwLock<std::sync::Arc<cljrs_project::vcs::TrustedKeys>>,
101}
102
103#[cfg(all(feature = "deps", not(target_arch = "wasm32")))]
104impl Default for ProjectVcs {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110#[cfg(all(feature = "deps", not(target_arch = "wasm32")))]
111impl ProjectVcs {
112 pub fn new() -> Self {
113 Self {
114 trusted: std::sync::RwLock::new(std::sync::Arc::new(
115 cljrs_project::vcs::TrustedKeys::new(),
116 )),
117 }
118 }
119}
120
121#[cfg(all(feature = "deps", not(target_arch = "wasm32")))]
122impl VcsProvider for ProjectVcs {
123 fn find_repo_root(&self, start: &Path) -> Option<PathBuf> {
124 cljrs_project::vcs::find_repo_root(start)
125 }
126
127 fn file_at_commit(
128 &self,
129 repo_root: &Path,
130 rel_path: &str,
131 commit: &str,
132 ) -> Result<String, String> {
133 cljrs_project::vcs::get_file_at_commit(repo_root, rel_path, commit)
134 .map_err(|e| e.to_string())
135 }
136
137 fn verify_commit_signature(
138 &self,
139 repo_root: &Path,
140 commit: &str,
141 ) -> Result<(), SignatureFailure> {
142 let trusted = self.trusted.read().unwrap().clone();
143 cljrs_project::vcs::verify_commit_signature(repo_root, commit, &trusted).map_err(
144 |e| match e {
145 cljrs_project::vcs::VcsError::SignatureVerificationFailed { commit, reason } => {
146 SignatureFailure::Untrusted { commit, reason }
147 }
148 other => SignatureFailure::Error(other.to_string()),
149 },
150 )
151 }
152
153 fn load_trusted_signers(&self, signers: &[TrustedSigner]) -> usize {
154 let mut keys = cljrs_project::vcs::TrustedKeys::new();
155 let mut loaded = 0usize;
156 for signer in signers {
157 let result = match signer {
158 TrustedSigner::Inline(text) => keys.add_key_text(text),
159 TrustedSigner::File(path) => match std::fs::read_to_string(path) {
160 Ok(text) => keys.add_key_text(&text),
161 Err(e) => {
162 eprintln!(
163 "cljrs: warning: could not read trusted signer key {}: {e}",
164 path.display()
165 );
166 continue;
167 }
168 },
169 };
170 match result {
171 Ok(()) => loaded += 1,
172 Err(e) => eprintln!("cljrs: warning: invalid trusted signer key: {e}"),
173 }
174 }
175 *self.trusted.write().unwrap() = std::sync::Arc::new(keys);
176 loaded
177 }
178}