Skip to main content

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}