Skip to main content

confium_registry/
manifest.rs

1//! Typed mirrors of the TOML documents served by the registry.
2//!
3//! These types correspond one-to-one with the schemas documented in
4//! `TODO.roadmap/06-module-registry.md`:
5//!
6//! - [`IndexEntry`] — one `[[plugin]]` row of the master `index.toml`.
7//! - [`PluginIndex`] — the per-plugin `index.toml` (a name, latest
8//!   pointer, and a list of [`VersionEntry`] rows).
9//! - [`Manifest`] — the per-version `manifest.toml`, with nested
10//!   [`ConfiumMeta`], [`AlgorithmMap`], and [`Artifact`] sections.
11//! - [`TrustRoot`] / [`TrustRootsFile`] — the default `trust-roots.toml`
12//!   served by the registry and the local override format.
13//!
14//! All types derive `serde::{Serialize, Deserialize}` so they can be
15//! round-tripped (registry reads use `Deserialize`; the local trust store
16//! and config use both). Field names match the wire (kebab-case) via
17//! `serde(rename_all = "kebab-case"`.
18
19use std::collections::BTreeMap;
20
21use serde::{Deserialize, Serialize};
22
23/// One `[[plugin]]` entry in the master `index.toml`.
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct IndexEntry {
26    pub name: String,
27    pub latest: String,
28    #[serde(default)]
29    pub description: String,
30    #[serde(default, rename = "publishers")]
31    pub publishers: Vec<String>,
32    #[serde(rename = "versions-url")]
33    pub versions_url: String,
34}
35
36/// The parsed master `index.toml`.
37#[derive(Debug, Clone, Serialize, Deserialize, Default)]
38pub struct RegistryIndex {
39    #[serde(default, rename = "plugin")]
40    pub plugins: Vec<IndexEntry>,
41}
42
43/// The per-plugin `index.toml`.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct PluginIndex {
46    pub name: String,
47    pub latest: String,
48    #[serde(default)]
49    pub description: String,
50    #[serde(default, rename = "version")]
51    pub versions: Vec<VersionEntry>,
52}
53
54/// One `[[version]]` entry inside a per-plugin index.
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
56pub struct VersionEntry {
57    pub version: String,
58    #[serde(rename = "manifest-url")]
59    pub manifest_url: String,
60}
61
62/// A full per-version manifest.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Manifest {
65    pub plugin: ManifestPlugin,
66    #[serde(default)]
67    pub confium: ConfiumMeta,
68    #[serde(default)]
69    pub dependencies: BTreeMap<String, String>,
70    #[serde(default)]
71    pub interfaces: BTreeMap<String, u32>,
72    #[serde(default)]
73    pub algorithms: AlgorithmMap,
74    pub artifact: Artifact,
75}
76
77/// The `[plugin]` section of a manifest.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ManifestPlugin {
80    pub name: String,
81    pub version: String,
82    #[serde(default)]
83    pub publisher: String,
84    #[serde(default)]
85    pub license: String,
86    #[serde(default)]
87    pub homepage: String,
88    #[serde(default)]
89    pub source: String,
90}
91
92/// The `[confium]` runtime contract section.
93#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94pub struct ConfiumMeta {
95    #[serde(rename = "contract-version", default)]
96    pub contract_version: u32,
97    #[serde(rename = "min-runtime", default)]
98    pub min_runtime: String,
99}
100
101/// Algorithms grouped by interface name. TOML tables whose values are
102/// arrays of strings.
103pub type AlgorithmMap = BTreeMap<String, Vec<String>>;
104
105/// The `[artifact]` section of a manifest.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Artifact {
108    #[serde(default)]
109    pub url: String,
110    #[serde(default)]
111    pub size: u64,
112    #[serde(default)]
113    pub sha256: String,
114    #[serde(default)]
115    pub mirrors: Vec<String>,
116}
117
118/// One `[[publisher]]` entry in `trust-roots.toml`.
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
120pub struct TrustRoot {
121    pub name: String,
122    #[serde(rename = "key-id")]
123    pub key_id: String,
124    pub fingerprint: String,
125    #[serde(rename = "key-url")]
126    pub key_url: String,
127}
128
129/// The parsed `trust-roots.toml`.
130#[derive(Debug, Clone, Serialize, Deserialize, Default)]
131pub struct TrustRootsFile {
132    #[serde(rename = "min-signatures", default = "default_min_signatures")]
133    pub min_signatures: u32,
134    #[serde(default, rename = "publisher")]
135    pub publishers: Vec<TrustRoot>,
136}
137
138fn default_min_signatures() -> u32 {
139    1
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    // Real registry fixtures — these mirror what's actually served
147    // at registry.confium.org and catch wire-format drift that
148    // inline-only test fixtures would miss.
149    const MASTER_FIXTURE: &str = include_str!("../../../sites/registry/index.toml");
150    const PLUGIN_FIXTURE: &str = include_str!("../../../sites/registry/plugins/botan/index.toml");
151
152    const SAMPLE_MANIFEST: &str = r#"
153[plugin]
154name = "botan"
155version = "3.2.0"
156publisher = "ribose"
157license = "BSD-2-Clause"
158
159[confium]
160contract-version = 0
161min-runtime = "0.3.0"
162
163[interfaces]
164hash = 0
165aead = 0
166
167[algorithms]
168hash = ["SHA-256", "SHA-512"]
169aead = ["AES-256-GCM"]
170
171[artifact]
172url = "https://example.com/libcfm-botan.dylib"
173size = 1234
174sha256 = "abcd"
175mirrors = ["https://mirror.example.com/x"]
176"#;
177
178    #[test]
179    fn manifest_round_trips() {
180        let manifest: Manifest = toml::from_str(SAMPLE_MANIFEST).expect("parse");
181        assert_eq!(manifest.plugin.name, "botan");
182        assert_eq!(manifest.plugin.version, "3.2.0");
183        assert_eq!(manifest.confium.contract_version, 0);
184        assert_eq!(manifest.interfaces.get("hash"), Some(&0));
185        assert_eq!(
186            manifest.algorithms.get("hash"),
187            Some(&vec!["SHA-256".to_string(), "SHA-512".to_string()])
188        );
189        assert_eq!(manifest.artifact.size, 1234);
190        assert_eq!(manifest.artifact.mirrors.len(), 1);
191    }
192
193    #[test]
194    fn registry_index_parses() {
195        let src = r#"
196[[plugin]]
197name = "botan"
198latest = "3.2.0"
199description = "Botan"
200publishers = ["ribose"]
201versions-url = "/plugins/botan/index.toml"
202"#;
203        let idx: RegistryIndex = toml::from_str(src).expect("parse");
204        assert_eq!(idx.plugins.len(), 1);
205        assert_eq!(idx.plugins[0].name, "botan");
206        assert_eq!(idx.plugins[0].publishers, vec!["ribose"]);
207    }
208
209    #[test]
210    fn trust_roots_parse_with_defaults() {
211        let src = r#"
212[[publisher]]
213name = "ribose"
214key-id = "0xABCD"
215fingerprint = "AAAA"
216key-url = "/publishers/ribose.asc"
217"#;
218        let roots: TrustRootsFile = toml::from_str(src).expect("parse");
219        assert_eq!(roots.min_signatures, 1);
220        assert_eq!(roots.publishers[0].name, "ribose");
221        assert_eq!(roots.publishers[0].key_id, "0xABCD");
222    }
223
224    #[test]
225    fn real_master_fixture_parses() {
226        let cat: RegistryIndex = toml::from_str(MASTER_FIXTURE).expect("master fixture parses");
227        let botan = cat
228            .plugins
229            .iter()
230            .find(|p| p.name == "botan")
231            .expect("botan entry present in fixture");
232        assert_eq!(botan.latest, "3.2.0");
233        assert_eq!(botan.publishers, vec!["ribose", "ni4"]);
234        assert_eq!(botan.versions_url, "/plugins/botan/index.toml");
235    }
236
237    #[test]
238    fn real_plugin_fixture_parses() {
239        let idx: PluginIndex = toml::from_str(PLUGIN_FIXTURE).expect("plugin fixture parses");
240        assert_eq!(idx.name, "botan");
241        assert_eq!(idx.latest, "3.2.0");
242        assert_eq!(idx.versions.len(), 1);
243        assert_eq!(
244            idx.versions[0].manifest_url,
245            "/plugins/botan/3.2.0/manifest.toml"
246        );
247    }
248
249    #[test]
250    fn real_plugin_fixture_resolves_latest() {
251        let idx: PluginIndex = toml::from_str(PLUGIN_FIXTURE).expect("plugin fixture parses");
252        let target = idx
253            .versions
254            .iter()
255            .find(|v| v.version == idx.latest)
256            .map(|v| v.manifest_url.as_str())
257            .expect("latest resolves");
258        assert_eq!(target, "/plugins/botan/3.2.0/manifest.toml");
259    }
260}