Skip to main content

vanta_provider/
lib.rs

1//! `vanta-provider` — the declarative provider model.
2//!
3//! A provider describes how to turn a `version` + `Platform` into a concrete
4//! [`Artifact`]: a URL template (with `{version}`/`{os}`/`{arch}`/`{ext}`
5//! placeholders), the archive kind, the bin paths, and per-token name maps that
6//! translate Vanta's canonical platform tokens into the upstream's spelling
7//! (e.g. `macos`→`darwin`, `aarch64`→`arm64`). See `docs/07-providers.md` and
8//! `docs/22-provider-sdk.md`.
9//!
10//! This is the declarative path (no code). Providers that need custom logic use a
11//! sandboxed WASM hook ([`Sandbox`], see `docs/22-provider-sdk.md`).
12#![forbid(unsafe_code)]
13
14pub mod wasm;
15pub use wasm::Sandbox;
16
17use serde::{Deserialize, Serialize};
18use std::collections::BTreeMap;
19use vanta_core::{Artifact, Checksum, Platform};
20
21/// A declarative provider definition (one tool).
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct ProviderDef {
24    /// Provider id, e.g. `"official/node"`.
25    pub id: String,
26    /// The tool this provider serves.
27    pub tool: String,
28    /// URL template with `{version}`/`{os}`/`{arch}`/`{ext}` placeholders.
29    pub url_template: String,
30    /// Archive kind: `tar.gz` / `tgz` / `zip` / `raw`.
31    pub archive: String,
32    /// Per-OS override of the archive kind, keyed by the *canonical* OS token
33    /// (`linux`/`macos`/`windows`). Some upstreams ship different formats per
34    /// OS (e.g. gh: linux `tar.gz`, macOS `zip`); an entry here overrides
35    /// [`ProviderDef::archive`] for that OS.
36    #[serde(default)]
37    pub archive_map: BTreeMap<String, String>,
38    /// Components to strip when materializing (recorded for the store layout).
39    #[serde(default)]
40    pub strip: u32,
41    /// Executables to expose (paths relative to the laid-out tree).
42    #[serde(default)]
43    pub bin: Vec<String>,
44    /// Map a canonical OS token to the upstream spelling (`macos` → `darwin`).
45    #[serde(default)]
46    pub os_map: BTreeMap<String, String>,
47    /// Map a canonical arch token to the upstream spelling (`aarch64` → `arm64`).
48    #[serde(default)]
49    pub arch_map: BTreeMap<String, String>,
50    /// Source-build recipe steps (each an argv). When present, the rendered
51    /// `url_template` points at a *source* tarball, and the install engine
52    /// compiles it with these steps instead of installing prebuilt bytes.
53    /// Placeholders `{prefix}`/`{jobs}` are substituted by the engine.
54    #[serde(default)]
55    pub build_steps: Option<Vec<Vec<String>>>,
56}
57
58impl ProviderDef {
59    /// Render the artifact for `version` on `platform`, attaching `checksum`.
60    /// `size` is optional metadata carried into the lock.
61    pub fn render_artifact(
62        &self,
63        version: &str,
64        platform: &Platform,
65        checksum: Checksum,
66        size: Option<u64>,
67    ) -> Artifact {
68        let os = self.map_os(platform);
69        let arch = self.map_arch(platform);
70        let archive = self.archive_for(platform);
71        // `{minor}` = the first two version components ("3.3.6" → "3.3"), for
72        // upstreams that lay releases out under a major.minor directory
73        // (e.g. ruby: `.../pub/ruby/{minor}/ruby-{version}.tar.gz`).
74        let minor = {
75            let mut it = version.split('.');
76            match (it.next(), it.next()) {
77                (Some(a), Some(b)) => format!("{a}.{b}"),
78                _ => version.to_string(),
79            }
80        };
81        let url = self
82            .url_template
83            .replace("{version}", version)
84            .replace("{minor}", &minor)
85            .replace("{os}", &os)
86            .replace("{arch}", &arch)
87            .replace("{ext}", ext_for(&archive));
88        Artifact {
89            url,
90            mirrors: Vec::new(),
91            archive,
92            size,
93            checksum,
94            signature: None,
95            signature_key: None,
96            bin: self.bin.clone(),
97            strip: self.strip,
98            build: self
99                .build_steps
100                .clone()
101                .map(|steps| vanta_core::BuildRecipe { steps }),
102            store_key: None,
103        }
104    }
105
106    /// The archive kind for `platform`: the per-OS override when present
107    /// (keyed by the canonical OS token), else the default [`Self::archive`].
108    pub fn archive_for(&self, platform: &Platform) -> String {
109        self.archive_map
110            .get(platform.os.as_str())
111            .cloned()
112            .unwrap_or_else(|| self.archive.clone())
113    }
114
115    fn map_os(&self, platform: &Platform) -> String {
116        let key = platform.os.as_str();
117        self.os_map
118            .get(key)
119            .cloned()
120            .unwrap_or_else(|| key.to_string())
121    }
122
123    fn map_arch(&self, platform: &Platform) -> String {
124        let key = platform.arch.as_str();
125        self.arch_map
126            .get(key)
127            .cloned()
128            .unwrap_or_else(|| key.to_string())
129    }
130}
131
132/// The file extension implied by an archive kind (for `{ext}` substitution).
133pub fn ext_for(archive: &str) -> &'static str {
134    match archive {
135        "tar.gz" | "tgz" => "tar.gz",
136        "tar.xz" => "tar.xz",
137        "zip" => "zip",
138        _ => "",
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use vanta_core::{Arch, Libc, Os};
146
147    fn node_provider() -> ProviderDef {
148        let mut os_map = BTreeMap::new();
149        os_map.insert("macos".into(), "darwin".into());
150        let mut arch_map = BTreeMap::new();
151        arch_map.insert("aarch64".into(), "arm64".into());
152        ProviderDef {
153            id: "official/node".into(),
154            tool: "node".into(),
155            url_template: "https://nodejs.org/dist/v{version}/node-v{version}-{os}-{arch}.{ext}"
156                .into(),
157            archive: "tar.gz".into(),
158            archive_map: BTreeMap::new(),
159            strip: 1,
160            bin: vec!["bin/node".into()],
161            os_map,
162            arch_map,
163            build_steps: None,
164        }
165    }
166
167    #[test]
168    fn archive_map_overrides_kind_and_ext_per_os() {
169        let mut p = node_provider();
170        p.archive_map.insert("macos".into(), "zip".into());
171        let mac = Platform {
172            os: Os::Macos,
173            arch: Arch::Aarch64,
174            libc: Libc::None,
175        };
176        let linux = Platform {
177            os: Os::Linux,
178            arch: Arch::X86_64,
179            libc: Libc::Gnu,
180        };
181        fn cs() -> Checksum {
182            Checksum {
183                algo: "sha256".into(),
184                value: "00".into(),
185            }
186        }
187        let mac_art = p.render_artifact("1.0.0", &mac, cs(), None);
188        let linux_art = p.render_artifact("1.0.0", &linux, cs(), None);
189        assert_eq!(mac_art.archive, "zip");
190        assert!(mac_art.url.ends_with(".zip"));
191        assert_eq!(linux_art.archive, "tar.gz");
192        assert!(linux_art.url.ends_with(".tar.gz"));
193    }
194
195    #[test]
196    fn renders_url_with_token_maps() {
197        let p = node_provider();
198        let plat = Platform {
199            os: Os::Macos,
200            arch: Arch::Aarch64,
201            libc: Libc::None,
202        };
203        let art = p.render_artifact(
204            "24.6.0",
205            &plat,
206            Checksum {
207                algo: "sha256".into(),
208                value: "abc".into(),
209            },
210            Some(100),
211        );
212        assert_eq!(
213            art.url,
214            "https://nodejs.org/dist/v24.6.0/node-v24.6.0-darwin-arm64.tar.gz"
215        );
216        assert_eq!(art.archive, "tar.gz");
217        assert_eq!(art.bin, vec!["bin/node".to_string()]);
218        assert_eq!(art.checksum.value, "abc");
219    }
220}