resolve.rs (15368B)
1 use crate::error::RadrootsRuntimeDistributionError; 2 use crate::model::{ 3 ArtifactAdapter, RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSpec, 4 }; 5 use crate::service::{HardenedServiceTarget, ServiceTier1Target}; 6 use crate::service_artifact::{HardenedServiceArtifact, ServiceArtifactSha256}; 7 use radroots_runtime_paths::ServiceId; 8 9 pub const RUNTIME_DISTRIBUTION_SCHEMA: &str = "radroots-runtime-distribution"; 10 pub const RUNTIME_DISTRIBUTION_SCHEMA_VERSION: u32 = 1; 11 pub const RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES: usize = 1_048_576; 12 13 #[derive(Debug, Clone, PartialEq, Eq)] 14 pub struct ServiceTargetRequest<'a> { 15 pub service_id: &'a ServiceId, 16 pub target_id: &'a str, 17 } 18 19 #[derive(Debug, Clone, PartialEq, Eq)] 20 pub struct ResolvedServiceTarget { 21 service_id: ServiceId, 22 target: ServiceTier1Target, 23 } 24 25 #[derive(Debug, Clone, PartialEq, Eq)] 26 pub struct ServiceArtifactRequest<'a> { 27 pub service_id: &'a ServiceId, 28 pub target_id: &'a str, 29 } 30 31 /// Exact native release artifact metadata for one hardened service target. 32 #[derive(Clone, PartialEq, Eq)] 33 pub struct ResolvedServiceArtifact { 34 service_id: ServiceId, 35 target: ServiceTier1Target, 36 version: String, 37 package_name: String, 38 binary_name: String, 39 channel: String, 40 binary_archive_name: String, 41 binary_archive_format: &'static str, 42 binary_archive_member: String, 43 artifact_manifest_name: String, 44 checksums_name: String, 45 checksum_algorithm: &'static str, 46 checksum_format: &'static str, 47 release_contract_sha256: ServiceArtifactSha256, 48 source_lock_sha256: ServiceArtifactSha256, 49 output_inventory: Vec<String>, 50 } 51 52 impl core::fmt::Debug for ResolvedServiceArtifact { 53 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 54 formatter 55 .debug_struct("ResolvedServiceArtifact") 56 .field("service_id", &self.service_id) 57 .field("target", &self.target) 58 .field("version", &self.version) 59 .field("package_name", &self.package_name) 60 .field("binary_name", &self.binary_name) 61 .field("channel", &self.channel) 62 .field("binary_archive_name", &self.binary_archive_name) 63 .field("binary_archive_format", &self.binary_archive_format) 64 .field("binary_archive_member", &self.binary_archive_member) 65 .field("artifact_manifest_name", &self.artifact_manifest_name) 66 .field("checksums_name", &self.checksums_name) 67 .field("checksum_algorithm", &self.checksum_algorithm) 68 .field("checksum_format", &self.checksum_format) 69 .field("release_contract_sha256", &"<redacted>") 70 .field("source_lock_sha256", &"<redacted>") 71 .field("output_inventory", &self.output_inventory) 72 .finish() 73 } 74 } 75 76 impl ResolvedServiceArtifact { 77 #[must_use] 78 pub fn service_id(&self) -> &ServiceId { 79 &self.service_id 80 } 81 #[must_use] 82 pub const fn target(&self) -> ServiceTier1Target { 83 self.target 84 } 85 #[must_use] 86 pub fn version(&self) -> &str { 87 &self.version 88 } 89 #[must_use] 90 pub fn package_name(&self) -> &str { 91 &self.package_name 92 } 93 #[must_use] 94 pub fn binary_name(&self) -> &str { 95 &self.binary_name 96 } 97 #[must_use] 98 pub fn channel(&self) -> &str { 99 &self.channel 100 } 101 #[must_use] 102 pub fn binary_archive_name(&self) -> &str { 103 &self.binary_archive_name 104 } 105 #[must_use] 106 pub const fn binary_archive_format(&self) -> &'static str { 107 self.binary_archive_format 108 } 109 #[must_use] 110 pub fn binary_archive_member(&self) -> &str { 111 &self.binary_archive_member 112 } 113 #[must_use] 114 pub fn artifact_manifest_name(&self) -> &str { 115 &self.artifact_manifest_name 116 } 117 #[must_use] 118 pub fn checksums_name(&self) -> &str { 119 &self.checksums_name 120 } 121 #[must_use] 122 pub const fn checksum_algorithm(&self) -> &'static str { 123 self.checksum_algorithm 124 } 125 #[must_use] 126 pub const fn checksum_format(&self) -> &'static str { 127 self.checksum_format 128 } 129 #[must_use] 130 pub const fn release_contract_sha256(&self) -> ServiceArtifactSha256 { 131 self.release_contract_sha256 132 } 133 #[must_use] 134 pub const fn source_lock_sha256(&self) -> ServiceArtifactSha256 { 135 self.source_lock_sha256 136 } 137 #[must_use] 138 pub fn output_inventory(&self) -> &[String] { 139 &self.output_inventory 140 } 141 } 142 143 impl ResolvedServiceTarget { 144 #[must_use] 145 pub fn service_id(&self) -> &ServiceId { 146 &self.service_id 147 } 148 149 #[must_use] 150 pub const fn target(&self) -> ServiceTier1Target { 151 self.target 152 } 153 } 154 155 #[derive(Debug, Clone, PartialEq, Eq)] 156 pub struct RuntimeArtifactRequest<'a> { 157 pub runtime_id: &'a str, 158 pub os: &'a str, 159 pub arch: &'a str, 160 pub version: &'a str, 161 pub channel: Option<&'a str>, 162 } 163 164 #[derive(Debug, Clone, PartialEq, Eq)] 165 pub struct ResolvedRuntimeArtifact { 166 pub runtime_id: String, 167 pub release_unit: String, 168 pub package_name: String, 169 pub binary_name: Option<String>, 170 pub artifact_adapter: String, 171 pub channel: String, 172 pub version: String, 173 pub target_id: String, 174 pub os: String, 175 pub arch: String, 176 pub archive_format: String, 177 pub archive_extension: String, 178 pub artifact_stem: String, 179 pub artifact_file_name: String, 180 } 181 182 #[derive(Debug, Clone)] 183 pub struct RadrootsRuntimeDistributionResolver { 184 contract: RadrootsRuntimeDistributionContract, 185 } 186 187 impl RadrootsRuntimeDistributionResolver { 188 pub fn parse_str(raw: &str) -> Result<Self, RadrootsRuntimeDistributionError> { 189 if raw.len() > RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES { 190 return Err(RadrootsRuntimeDistributionError::ContractTooLarge); 191 } 192 let contract = toml::from_str::<RadrootsRuntimeDistributionContract>(raw) 193 .map_err(|_| RadrootsRuntimeDistributionError::Parse)?; 194 Self::new(contract) 195 } 196 197 pub fn new( 198 contract: RadrootsRuntimeDistributionContract, 199 ) -> Result<Self, RadrootsRuntimeDistributionError> { 200 if contract.schema != RUNTIME_DISTRIBUTION_SCHEMA { 201 return Err(RadrootsRuntimeDistributionError::UnexpectedSchema); 202 } 203 if contract.schema_version != RUNTIME_DISTRIBUTION_SCHEMA_VERSION { 204 return Err(RadrootsRuntimeDistributionError::UnexpectedSchemaVersion); 205 } 206 if contract.runtime.iter().any(|runtime| { 207 contract 208 .service_targets 209 .iter() 210 .any(|(_, service)| runtime.id == service.service_id().as_str()) 211 }) { 212 return Err(RadrootsRuntimeDistributionError::HardenedServiceLegacyArtifactRow); 213 } 214 if !contract 215 .channels 216 .active 217 .iter() 218 .any(|channel| channel == "stable") 219 || !contract 220 .channels 221 .defined 222 .iter() 223 .any(|channel| channel == "stable") 224 { 225 return Err(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract); 226 } 227 for (service_id, target) in contract.service_targets.iter() { 228 let artifact = contract 229 .service_artifacts 230 .get(target.service_id()) 231 .ok_or(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract)?; 232 if service_id != artifact.service_id().as_str() 233 || target.tier_1_targets() != artifact.tier_1_targets() 234 { 235 return Err(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract); 236 } 237 } 238 Ok(Self { contract }) 239 } 240 241 pub fn contract(&self) -> &RadrootsRuntimeDistributionContract { 242 &self.contract 243 } 244 245 pub fn service_target( 246 &self, 247 service_id: &ServiceId, 248 ) -> Result<&HardenedServiceTarget, RadrootsRuntimeDistributionError> { 249 self.contract 250 .service_targets 251 .get(service_id) 252 .ok_or(RadrootsRuntimeDistributionError::UnsupportedService) 253 } 254 255 pub fn resolve_service_target( 256 &self, 257 request: &ServiceTargetRequest<'_>, 258 ) -> Result<ResolvedServiceTarget, RadrootsRuntimeDistributionError> { 259 let service = self.service_target(request.service_id)?; 260 let target = ServiceTier1Target::parse(request.target_id) 261 .filter(|target| service.tier_1_targets().contains(target)) 262 .ok_or(RadrootsRuntimeDistributionError::UnsupportedServiceTarget)?; 263 Ok(ResolvedServiceTarget { 264 service_id: request.service_id.clone(), 265 target, 266 }) 267 } 268 269 pub fn service_artifact( 270 &self, 271 service_id: &ServiceId, 272 ) -> Result<&HardenedServiceArtifact, RadrootsRuntimeDistributionError> { 273 self.contract 274 .service_artifacts 275 .get(service_id) 276 .ok_or(RadrootsRuntimeDistributionError::UnsupportedService) 277 } 278 279 pub fn resolve_service_artifact( 280 &self, 281 request: &ServiceArtifactRequest<'_>, 282 ) -> Result<ResolvedServiceArtifact, RadrootsRuntimeDistributionError> { 283 let artifact = self.service_artifact(request.service_id)?; 284 let target = ServiceTier1Target::parse(request.target_id) 285 .filter(|target| artifact.tier_1_targets().contains(target)) 286 .ok_or(RadrootsRuntimeDistributionError::UnsupportedServiceTarget)?; 287 let service = artifact.service_id().as_str(); 288 let version = artifact.version(); 289 let binary = artifact.binary_name(); 290 Ok(ResolvedServiceArtifact { 291 service_id: request.service_id.clone(), 292 target, 293 version: version.to_owned(), 294 package_name: artifact.package_name().to_owned(), 295 binary_name: binary.to_owned(), 296 channel: artifact.channel().as_str().to_owned(), 297 binary_archive_name: artifact.binary_archive_name().to_owned(), 298 binary_archive_format: "tar.gz", 299 binary_archive_member: format!("{service}-{version}-{}/{binary}", target.as_str()), 300 artifact_manifest_name: artifact.artifact_manifest_name().to_owned(), 301 checksums_name: artifact.checksums_name().to_owned(), 302 checksum_algorithm: "sha256", 303 checksum_format: "sha256_lower_hex_two_spaces_path_lf_sorted_by_path", 304 release_contract_sha256: artifact.release_contract_sha256(), 305 source_lock_sha256: artifact.source_lock_sha256(), 306 output_inventory: artifact.output_inventory().to_vec(), 307 }) 308 } 309 310 pub fn resolve_artifact( 311 &self, 312 request: &RuntimeArtifactRequest<'_>, 313 ) -> Result<ResolvedRuntimeArtifact, RadrootsRuntimeDistributionError> { 314 let runtime = self 315 .contract 316 .runtime 317 .iter() 318 .find(|runtime| runtime.id == request.runtime_id) 319 .ok_or(RadrootsRuntimeDistributionError::UnknownRuntime)?; 320 321 if !runtime.human_installable { 322 return Err(RadrootsRuntimeDistributionError::RuntimeNotInstallable); 323 } 324 325 let channel = request.channel.unwrap_or(runtime.default_channel.as_str()); 326 self.ensure_channel_is_active(channel)?; 327 328 let target_set_id = runtime 329 .target_set 330 .as_ref() 331 .ok_or(RadrootsRuntimeDistributionError::MissingTargetSet)?; 332 333 let adapter = self 334 .contract 335 .artifact_adapters 336 .get(&runtime.artifact_adapter) 337 .ok_or(RadrootsRuntimeDistributionError::UnknownArtifactAdapter)?; 338 339 let (target_id, target) = 340 self.select_target(runtime, target_set_id, request.os, request.arch)?; 341 let archive_format_id = 342 self.resolve_archive_format_id(runtime, target_id, target, adapter)?; 343 let archive_format = self 344 .contract 345 .archive_formats 346 .get(&normalized_contract_key(archive_format_id)) 347 .ok_or(RadrootsRuntimeDistributionError::UnknownArchiveFormat)?; 348 349 let artifact_stem = format!("{}-{}-{}", runtime.release_unit, request.version, target_id); 350 let artifact_file_name = format!("{artifact_stem}{}", archive_format.extension); 351 352 Ok(ResolvedRuntimeArtifact { 353 runtime_id: runtime.id.clone(), 354 release_unit: runtime.release_unit.clone(), 355 package_name: runtime.package_name.clone(), 356 binary_name: runtime.binary_name.clone(), 357 artifact_adapter: runtime.artifact_adapter.clone(), 358 channel: channel.to_string(), 359 version: request.version.to_string(), 360 target_id: target_id.to_string(), 361 os: request.os.to_string(), 362 arch: request.arch.to_string(), 363 archive_format: archive_format_id.to_string(), 364 archive_extension: archive_format.extension.clone(), 365 artifact_stem, 366 artifact_file_name, 367 }) 368 } 369 370 fn ensure_channel_is_active( 371 &self, 372 channel: &str, 373 ) -> Result<(), RadrootsRuntimeDistributionError> { 374 if !self 375 .contract 376 .channels 377 .defined 378 .iter() 379 .any(|entry| entry == channel) 380 { 381 return Err(RadrootsRuntimeDistributionError::UnknownChannel); 382 } 383 if !self 384 .contract 385 .channels 386 .active 387 .iter() 388 .any(|entry| entry == channel) 389 { 390 return Err(RadrootsRuntimeDistributionError::InactiveChannel); 391 } 392 Ok(()) 393 } 394 395 fn select_target<'a>( 396 &'a self, 397 _runtime: &RuntimeDistributionEntry, 398 target_set_id: &str, 399 os: &str, 400 arch: &str, 401 ) -> Result<(&'a str, &'a TargetSpec), RadrootsRuntimeDistributionError> { 402 let target_set = self 403 .contract 404 .target_sets 405 .get(target_set_id) 406 .ok_or(RadrootsRuntimeDistributionError::UnsupportedPlatform)?; 407 408 let mut found_match = None; 409 for target_id in &target_set.targets { 410 let target = self 411 .contract 412 .targets 413 .get(target_id) 414 .ok_or(RadrootsRuntimeDistributionError::UnknownTarget)?; 415 416 if target.os == os && target.arch == arch { 417 found_match = Some((target_id.as_str(), target)); 418 break; 419 } 420 } 421 422 found_match.ok_or(RadrootsRuntimeDistributionError::UnsupportedPlatform) 423 } 424 425 fn resolve_archive_format_id<'a>( 426 &self, 427 _runtime: &RuntimeDistributionEntry, 428 _target_id: &'a str, 429 target: &'a TargetSpec, 430 adapter: &'a ArtifactAdapter, 431 ) -> Result<&'a str, RadrootsRuntimeDistributionError> { 432 if let Some(format) = target.archive_format.as_deref() { 433 return Ok(format); 434 } 435 436 if adapter.supported_archive_formats.len() == 1 { 437 return Ok(adapter.supported_archive_formats[0].as_str()); 438 } 439 440 Err(RadrootsRuntimeDistributionError::MissingArchiveFormat) 441 } 442 } 443 444 fn normalized_contract_key(value: &str) -> String { 445 value.replace('.', "_") 446 }