1use super::ExperimentError;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SovereignDistribution {
11 pub name: String,
13 pub version: String,
15 pub platforms: Vec<String>,
17 pub artifacts: Vec<SovereignArtifact>,
19 pub signatures: Vec<ArtifactSignature>,
21 pub offline_registry: Option<OfflineRegistryConfig>,
23 pub nix_flake_hash: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SovereignArtifact {
30 pub name: String,
32 pub artifact_type: ArtifactType,
34 pub sha256: String,
36 pub size_bytes: u64,
38 pub source_url: Option<String>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44pub enum ArtifactType {
45 Binary,
46 Model,
47 Dataset,
48 Config,
49 Documentation,
50 Container,
51 NixDerivation,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ArtifactSignature {
57 pub artifact_name: String,
59 pub algorithm: SignatureAlgorithm,
61 pub signature: String,
63 pub key_id: String,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69pub enum SignatureAlgorithm {
70 Ed25519,
71 RSA4096,
72 EcdsaP256,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct OfflineRegistryConfig {
78 pub path: String,
80 pub index_path: String,
82 pub platforms: Vec<String>,
84 pub last_sync: Option<String>,
86}
87
88impl SovereignDistribution {
89 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
91 Self {
92 name: name.into(),
93 version: version.into(),
94 platforms: Vec::new(),
95 artifacts: Vec::new(),
96 signatures: Vec::new(),
97 offline_registry: None,
98 nix_flake_hash: None,
99 }
100 }
101
102 pub fn add_platform(&mut self, platform: impl Into<String>) {
104 self.platforms.push(platform.into());
105 }
106
107 pub fn add_artifact(&mut self, artifact: SovereignArtifact) {
109 self.artifacts.push(artifact);
110 }
111
112 pub fn validate_signatures(&self) -> Result<(), ExperimentError> {
114 for artifact in &self.artifacts {
115 let has_sig = self.signatures.iter().any(|s| s.artifact_name == artifact.name);
116 if !has_sig {
117 return Err(ExperimentError::SovereignValidationFailed(format!(
118 "Missing signature for artifact: {}",
119 artifact.name
120 )));
121 }
122 }
123 Ok(())
124 }
125
126 pub fn total_size_bytes(&self) -> u64 {
128 self.artifacts.iter().map(|a| a.size_bytes).sum()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_sovereign_distribution_new() {
138 let dist = SovereignDistribution::new("my-dist", "1.0.0");
139 assert_eq!(dist.name, "my-dist");
140 assert_eq!(dist.version, "1.0.0");
141 assert!(dist.platforms.is_empty());
142 assert!(dist.artifacts.is_empty());
143 assert!(dist.signatures.is_empty());
144 assert!(dist.offline_registry.is_none());
145 assert!(dist.nix_flake_hash.is_none());
146 }
147
148 #[test]
149 fn test_add_platform() {
150 let mut dist = SovereignDistribution::new("test", "1.0");
151 dist.add_platform("linux-x86_64");
152 dist.add_platform("darwin-aarch64");
153 assert_eq!(dist.platforms.len(), 2);
154 assert!(dist.platforms.contains(&"linux-x86_64".to_string()));
155 assert!(dist.platforms.contains(&"darwin-aarch64".to_string()));
156 }
157
158 #[test]
159 fn test_add_artifact() {
160 let mut dist = SovereignDistribution::new("test", "1.0");
161 let artifact = SovereignArtifact {
162 name: "model.apr".to_string(),
163 artifact_type: ArtifactType::Model,
164 sha256: "abc123".to_string(),
165 size_bytes: 1024,
166 source_url: None,
167 };
168 dist.add_artifact(artifact);
169 assert_eq!(dist.artifacts.len(), 1);
170 assert_eq!(dist.artifacts[0].name, "model.apr");
171 }
172
173 #[test]
174 fn test_total_size_bytes() {
175 let mut dist = SovereignDistribution::new("test", "1.0");
176 dist.add_artifact(SovereignArtifact {
177 name: "a.bin".to_string(),
178 artifact_type: ArtifactType::Binary,
179 sha256: "hash1".to_string(),
180 size_bytes: 1000,
181 source_url: None,
182 });
183 dist.add_artifact(SovereignArtifact {
184 name: "b.model".to_string(),
185 artifact_type: ArtifactType::Model,
186 sha256: "hash2".to_string(),
187 size_bytes: 2000,
188 source_url: None,
189 });
190 assert_eq!(dist.total_size_bytes(), 3000);
191 }
192
193 #[test]
194 fn test_total_size_bytes_empty() {
195 let dist = SovereignDistribution::new("test", "1.0");
196 assert_eq!(dist.total_size_bytes(), 0);
197 }
198
199 #[test]
200 fn test_validate_signatures_missing() {
201 let mut dist = SovereignDistribution::new("test", "1.0");
202 dist.add_artifact(SovereignArtifact {
203 name: "unsigned.bin".to_string(),
204 artifact_type: ArtifactType::Binary,
205 sha256: "hash".to_string(),
206 size_bytes: 100,
207 source_url: None,
208 });
209 let result = dist.validate_signatures();
210 assert!(result.is_err());
211 }
212
213 #[test]
214 fn test_validate_signatures_valid() {
215 let mut dist = SovereignDistribution::new("test", "1.0");
216 dist.add_artifact(SovereignArtifact {
217 name: "signed.bin".to_string(),
218 artifact_type: ArtifactType::Binary,
219 sha256: "hash".to_string(),
220 size_bytes: 100,
221 source_url: None,
222 });
223 dist.signatures.push(ArtifactSignature {
224 artifact_name: "signed.bin".to_string(),
225 algorithm: SignatureAlgorithm::Ed25519,
226 signature: "base64sig".to_string(),
227 key_id: "key123".to_string(),
228 });
229 assert!(dist.validate_signatures().is_ok());
230 }
231
232 #[test]
233 fn test_validate_signatures_empty_dist() {
234 let dist = SovereignDistribution::new("test", "1.0");
235 assert!(dist.validate_signatures().is_ok());
236 }
237
238 #[test]
239 fn test_artifact_type_serialize() {
240 assert_eq!(
241 serde_json::to_string(&ArtifactType::Binary).expect("json serialize failed"),
242 "\"Binary\""
243 );
244 assert_eq!(
245 serde_json::to_string(&ArtifactType::Model).expect("json serialize failed"),
246 "\"Model\""
247 );
248 assert_eq!(
249 serde_json::to_string(&ArtifactType::NixDerivation).expect("json serialize failed"),
250 "\"NixDerivation\""
251 );
252 }
253
254 #[test]
255 fn test_signature_algorithm_equality() {
256 assert_eq!(SignatureAlgorithm::Ed25519, SignatureAlgorithm::Ed25519);
257 assert_ne!(SignatureAlgorithm::Ed25519, SignatureAlgorithm::RSA4096);
258 }
259
260 #[test]
261 fn test_sovereign_artifact_source_url() {
262 let artifact = SovereignArtifact {
263 name: "model.apr".to_string(),
264 artifact_type: ArtifactType::Model,
265 sha256: "abc123".to_string(),
266 size_bytes: 1024,
267 source_url: Some("https://example.com/model.apr".to_string()),
268 };
269 assert!(artifact.source_url.is_some());
270 assert_eq!(
271 artifact.source_url.expect("unexpected failure"),
272 "https://example.com/model.apr"
273 );
274 }
275
276 #[test]
277 fn test_offline_registry_config() {
278 let config = OfflineRegistryConfig {
279 path: "/opt/registry".to_string(),
280 index_path: "/opt/registry/index.json".to_string(),
281 platforms: vec!["linux-x86_64".to_string()],
282 last_sync: Some("2025-01-01T00:00:00Z".to_string()),
283 };
284 assert_eq!(config.path, "/opt/registry");
285 assert_eq!(config.platforms.len(), 1);
286 }
287
288 #[test]
289 fn test_distribution_with_registry() {
290 let mut dist = SovereignDistribution::new("air-gapped", "2.0");
291 dist.offline_registry = Some(OfflineRegistryConfig {
292 path: "/mnt/data".to_string(),
293 index_path: "/mnt/data/index.json".to_string(),
294 platforms: vec!["linux-x86_64".to_string(), "darwin-aarch64".to_string()],
295 last_sync: None,
296 });
297 assert!(dist.offline_registry.is_some());
298 }
299
300 #[test]
301 fn test_distribution_with_nix_hash() {
302 let mut dist = SovereignDistribution::new("reproducible", "1.0");
303 dist.nix_flake_hash = Some("sha256-abc123".to_string());
304 assert_eq!(dist.nix_flake_hash, Some("sha256-abc123".to_string()));
305 }
306}