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}
51
52impl ProviderDef {
53    /// Render the artifact for `version` on `platform`, attaching `checksum`.
54    /// `size` is optional metadata carried into the lock.
55    pub fn render_artifact(
56        &self,
57        version: &str,
58        platform: &Platform,
59        checksum: Checksum,
60        size: Option<u64>,
61    ) -> Artifact {
62        let os = self.map_os(platform);
63        let arch = self.map_arch(platform);
64        let archive = self.archive_for(platform);
65        let url = self
66            .url_template
67            .replace("{version}", version)
68            .replace("{os}", &os)
69            .replace("{arch}", &arch)
70            .replace("{ext}", ext_for(&archive));
71        Artifact {
72            url,
73            mirrors: Vec::new(),
74            archive,
75            size,
76            checksum,
77            signature: None,
78            signature_key: None,
79            bin: self.bin.clone(),
80            strip: self.strip,
81            store_key: None,
82        }
83    }
84
85    /// The archive kind for `platform`: the per-OS override when present
86    /// (keyed by the canonical OS token), else the default [`Self::archive`].
87    pub fn archive_for(&self, platform: &Platform) -> String {
88        self.archive_map
89            .get(platform.os.as_str())
90            .cloned()
91            .unwrap_or_else(|| self.archive.clone())
92    }
93
94    fn map_os(&self, platform: &Platform) -> String {
95        let key = platform.os.as_str();
96        self.os_map
97            .get(key)
98            .cloned()
99            .unwrap_or_else(|| key.to_string())
100    }
101
102    fn map_arch(&self, platform: &Platform) -> String {
103        let key = platform.arch.as_str();
104        self.arch_map
105            .get(key)
106            .cloned()
107            .unwrap_or_else(|| key.to_string())
108    }
109}
110
111/// The file extension implied by an archive kind (for `{ext}` substitution).
112pub fn ext_for(archive: &str) -> &'static str {
113    match archive {
114        "tar.gz" | "tgz" => "tar.gz",
115        "tar.xz" => "tar.xz",
116        "zip" => "zip",
117        _ => "",
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use vanta_core::{Arch, Libc, Os};
125
126    fn node_provider() -> ProviderDef {
127        let mut os_map = BTreeMap::new();
128        os_map.insert("macos".into(), "darwin".into());
129        let mut arch_map = BTreeMap::new();
130        arch_map.insert("aarch64".into(), "arm64".into());
131        ProviderDef {
132            id: "official/node".into(),
133            tool: "node".into(),
134            url_template: "https://nodejs.org/dist/v{version}/node-v{version}-{os}-{arch}.{ext}"
135                .into(),
136            archive: "tar.gz".into(),
137            archive_map: BTreeMap::new(),
138            strip: 1,
139            bin: vec!["bin/node".into()],
140            os_map,
141            arch_map,
142        }
143    }
144
145    #[test]
146    fn archive_map_overrides_kind_and_ext_per_os() {
147        let mut p = node_provider();
148        p.archive_map.insert("macos".into(), "zip".into());
149        let mac = Platform {
150            os: Os::Macos,
151            arch: Arch::Aarch64,
152            libc: Libc::None,
153        };
154        let linux = Platform {
155            os: Os::Linux,
156            arch: Arch::X86_64,
157            libc: Libc::Gnu,
158        };
159        fn cs() -> Checksum {
160            Checksum {
161                algo: "sha256".into(),
162                value: "00".into(),
163            }
164        }
165        let mac_art = p.render_artifact("1.0.0", &mac, cs(), None);
166        let linux_art = p.render_artifact("1.0.0", &linux, cs(), None);
167        assert_eq!(mac_art.archive, "zip");
168        assert!(mac_art.url.ends_with(".zip"));
169        assert_eq!(linux_art.archive, "tar.gz");
170        assert!(linux_art.url.ends_with(".tar.gz"));
171    }
172
173    #[test]
174    fn renders_url_with_token_maps() {
175        let p = node_provider();
176        let plat = Platform {
177            os: Os::Macos,
178            arch: Arch::Aarch64,
179            libc: Libc::None,
180        };
181        let art = p.render_artifact(
182            "24.6.0",
183            &plat,
184            Checksum {
185                algo: "sha256".into(),
186                value: "abc".into(),
187            },
188            Some(100),
189        );
190        assert_eq!(
191            art.url,
192            "https://nodejs.org/dist/v24.6.0/node-v24.6.0-darwin-arm64.tar.gz"
193        );
194        assert_eq!(art.archive, "tar.gz");
195        assert_eq!(art.bin, vec!["bin/node".to_string()]);
196        assert_eq!(art.checksum.value, "abc");
197    }
198}