1mod persistence;
8#[cfg(test)]
9mod tests;
10
11use crate::release_set::{
12 artifact::{RepresentationError, qualify_representation},
13 fixture::{FixtureArtifactError, FixtureArtifactManifest},
14 valid_package_name, validate_release_artifact_relative_path,
15};
16use std::collections::{BTreeMap, BTreeSet};
17
18use canic_core::{
19 bootstrap::compiled::{ComponentTopology, ComponentTopologyError},
20 cdk::utils::hash::decode_hex,
21 ids::{
22 CanisterRole, ComponentSpecId, ComponentTopologyDigest, FleetSubnetRootBinding,
23 ReleaseBuildId, ReleaseSetDigest,
24 },
25 role_contract::ProtocolProfileDigest,
26};
27use serde::{Deserialize, Serialize};
28use sha2_host::{Digest, Sha256};
29use thiserror::Error as ThisError;
30
31pub use persistence::load_retained_application_artifact_union;
32
33pub use persistence::{
34 ApplicationArtifactFileBuildOutput, ApplicationArtifactUnionPersistenceError,
35 PersistedApplicationArtifactUnion, compile_and_persist_application_artifact_union,
36 load_persisted_application_artifact_union,
37};
38
39const MAX_ARTIFACT_PATH_BYTES: usize = 4_096;
40const SHA_256_HEX_BYTES: usize = 64;
41
42#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct ApplicationArtifactBuildTarget {
50 pub role: CanisterRole,
51 pub package: String,
52 pub wasm_relative_path: String,
53 pub wasm_gz_relative_path: String,
54}
55
56#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct ApplicationArtifactBuildOutput {
64 pub role: CanisterRole,
65 pub package: String,
66 pub release_build_id: ReleaseBuildId,
67 pub wasm_relative_path: String,
68 pub wasm: Vec<u8>,
69 pub wasm_gz_relative_path: String,
70 pub wasm_gz: Vec<u8>,
71 pub candid_sha256: [u8; 32],
72 pub protocol_profile_digest: ProtocolProfileDigest,
73}
74
75#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
82#[serde(deny_unknown_fields)]
83pub struct ApplicationArtifactEntry {
84 pub role: CanisterRole,
85 pub package: String,
86 pub release_build_id: ReleaseBuildId,
87 pub wasm_relative_path: String,
88 pub wasm_size_bytes: u64,
89 pub wasm_sha256_hex: String,
90 pub wasm_gz_relative_path: String,
91 pub wasm_gz_size_bytes: u64,
92 pub wasm_gz_sha256_hex: String,
93 pub candid_sha256: [u8; 32],
94 pub protocol_profile_digest: ProtocolProfileDigest,
95}
96
97#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
104#[serde(deny_unknown_fields)]
105pub struct ApplicationArtifactUnion {
106 pub release_build_id: ReleaseBuildId,
107 pub fleet_component_topology_digest: ComponentTopologyDigest,
108 pub entries: Vec<ApplicationArtifactEntry>,
109}
110
111impl ApplicationArtifactUnion {
112 pub fn compile(
114 topology: &ComponentTopology,
115 release_build_id: ReleaseBuildId,
116 targets: &[ApplicationArtifactBuildTarget],
117 outputs: &[ApplicationArtifactBuildOutput],
118 ) -> Result<Self, ApplicationReleaseSetError> {
119 Self::validate_build_targets(topology, targets)?;
120 let targets = targets
121 .iter()
122 .map(|target| (&target.role, target))
123 .collect::<BTreeMap<_, _>>();
124
125 let mut output_roles = outputs
126 .iter()
127 .map(|output| output.role.clone())
128 .collect::<Vec<_>>();
129 Self::validate_build_output_roles(topology, &mut output_roles)?;
130
131 let mut entries = outputs
132 .iter()
133 .map(|output| {
134 let target = targets.get(&output.role).copied().ok_or_else(|| {
135 ApplicationReleaseSetError::MissingBuildTarget {
136 role: output.role.clone(),
137 }
138 })?;
139 compile_entry(release_build_id, target, output)
140 })
141 .collect::<Result<Vec<_>, _>>()?;
142 entries.sort_unstable_by(|left, right| left.role.cmp(&right.role));
143 let union = Self {
144 release_build_id,
145 fleet_component_topology_digest: topology.digest()?,
146 entries,
147 };
148 union.validate_against(topology)?;
149 Ok(union)
150 }
151
152 fn validate_build_targets(
153 topology: &ComponentTopology,
154 targets: &[ApplicationArtifactBuildTarget],
155 ) -> Result<(), ApplicationReleaseSetError> {
156 let expected_roles = topology_roles(topology);
157 let mut target_roles = targets
158 .iter()
159 .map(|target| target.role.clone())
160 .collect::<Vec<_>>();
161 target_roles.sort();
162 if target_roles != expected_roles {
163 return Err(ApplicationReleaseSetError::BuildTargetRoleSet {
164 expected: expected_roles,
165 actual: target_roles,
166 });
167 }
168 Ok(())
169 }
170
171 fn validate_build_output_roles(
172 topology: &ComponentTopology,
173 actual: &mut Vec<CanisterRole>,
174 ) -> Result<(), ApplicationReleaseSetError> {
175 let expected = topology_roles(topology);
176 actual.sort();
177 if *actual != expected {
178 return Err(ApplicationReleaseSetError::BuildOutputRoleSet {
179 expected,
180 actual: actual.clone(),
181 });
182 }
183 Ok(())
184 }
185
186 pub fn validate_against(
188 &self,
189 topology: &ComponentTopology,
190 ) -> Result<(), ApplicationReleaseSetError> {
191 self.validate_retained_shape()?;
192 let digest = topology.digest()?;
193 if self.fleet_component_topology_digest != digest {
194 return Err(ApplicationReleaseSetError::UnionTopologyDigestMismatch {
195 expected: digest,
196 actual: self.fleet_component_topology_digest,
197 });
198 }
199
200 let expected = topology_roles(topology);
201 let actual = self
202 .entries
203 .iter()
204 .map(|entry| entry.role.clone())
205 .collect::<Vec<_>>();
206 if actual != expected {
207 return Err(ApplicationReleaseSetError::ArtifactUnionRoleSet { expected, actual });
208 }
209 Ok(())
210 }
211
212 pub fn canonical_bytes(
214 &self,
215 topology: &ComponentTopology,
216 ) -> Result<Vec<u8>, ApplicationReleaseSetError> {
217 self.validate_against(topology)?;
218 serde_json::to_vec(self).map_err(ApplicationReleaseSetError::Serialization)
219 }
220
221 pub fn digest(
223 &self,
224 topology: &ComponentTopology,
225 ) -> Result<[u8; 32], ApplicationReleaseSetError> {
226 Ok(Sha256::digest(self.canonical_bytes(topology)?).into())
227 }
228
229 pub(crate) fn validate_retained_shape(&self) -> Result<(), ApplicationReleaseSetError> {
234 if self.entries.is_empty() {
235 return Err(ApplicationReleaseSetError::EmptyArtifactUnion);
236 }
237
238 let mut previous: Option<&CanisterRole> = None;
239 let mut paths = BTreeSet::new();
240 for entry in &self.entries {
241 if let Some(previous) = previous
242 && previous >= &entry.role
243 {
244 return Err(ApplicationReleaseSetError::NonCanonicalArtifactOrder {
245 previous: previous.clone(),
246 current: entry.role.clone(),
247 });
248 }
249 previous = Some(&entry.role);
250 validate_entry(self.release_build_id, entry)?;
251 for path in [&entry.wasm_relative_path, &entry.wasm_gz_relative_path] {
252 if !paths.insert(path.as_str()) {
253 return Err(ApplicationReleaseSetError::DuplicateArtifactPath {
254 path: path.clone(),
255 });
256 }
257 }
258 }
259 Ok(())
260 }
261
262 fn get(&self, role: &CanisterRole) -> Option<&ApplicationArtifactEntry> {
263 self.entries
264 .binary_search_by(|entry| entry.role.cmp(role))
265 .ok()
266 .map(|index| &self.entries[index])
267 }
268}
269
270#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
277#[serde(rename_all = "snake_case")]
278pub enum ApplicationReleaseSetEntryKind {
279 Component,
280 ComponentChild,
281}
282
283#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
290#[serde(deny_unknown_fields)]
291pub struct FleetSubnetRootReleaseSetEntry {
292 pub component_spec: ComponentSpecId,
293 pub kind: ApplicationReleaseSetEntryKind,
294 pub artifact: ApplicationArtifactEntry,
295}
296
297#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
304#[serde(deny_unknown_fields)]
305pub struct FleetSubnetRootReleaseSetManifest {
306 pub release_build_id: ReleaseBuildId,
307 pub component_topology_digest: ComponentTopologyDigest,
308 pub entries: Vec<FleetSubnetRootReleaseSetEntry>,
309 pub fixtures: Vec<canic_core::dto::root_store::RootStoreFixture>,
310}
311
312impl FleetSubnetRootReleaseSetManifest {
313 #[must_use]
315 pub fn root_store_manifest(&self) -> canic_core::dto::root_store::RootStoreReleaseSetManifest {
316 use canic_core::dto::root_store::{
317 RootStoreArtifact, RootStoreReleaseSetEntry, RootStoreReleaseSetEntryKind,
318 RootStoreReleaseSetManifest,
319 };
320
321 RootStoreReleaseSetManifest {
322 fixtures: self.fixtures.clone(),
323 release_build_id: self.release_build_id,
324 component_topology_digest: self.component_topology_digest,
325 entries: self
326 .entries
327 .iter()
328 .map(|entry| RootStoreReleaseSetEntry {
329 component_spec: entry.component_spec.clone(),
330 kind: match entry.kind {
331 ApplicationReleaseSetEntryKind::Component => {
332 RootStoreReleaseSetEntryKind::Component
333 }
334 ApplicationReleaseSetEntryKind::ComponentChild => {
335 RootStoreReleaseSetEntryKind::ComponentChild
336 }
337 },
338 artifact: RootStoreArtifact {
339 role: entry.artifact.role.clone(),
340 package: entry.artifact.package.clone(),
341 release_build_id: entry.artifact.release_build_id,
342 wasm_relative_path: entry.artifact.wasm_relative_path.clone(),
343 wasm_size_bytes: entry.artifact.wasm_size_bytes,
344 wasm_sha256_hex: entry.artifact.wasm_sha256_hex.clone(),
345 wasm_gz_relative_path: entry.artifact.wasm_gz_relative_path.clone(),
346 wasm_gz_size_bytes: entry.artifact.wasm_gz_size_bytes,
347 wasm_gz_sha256_hex: entry.artifact.wasm_gz_sha256_hex.clone(),
348 candid_sha256: entry.artifact.candid_sha256,
349 protocol_profile_digest: entry.artifact.protocol_profile_digest,
350 },
351 })
352 .collect(),
353 }
354 }
355
356 pub fn project(
358 topology: &ComponentTopology,
359 binding: &FleetSubnetRootBinding,
360 union: &ApplicationArtifactUnion,
361 fixtures: &FixtureArtifactManifest,
362 ) -> Result<Self, ApplicationReleaseSetError> {
363 Self::project_for_root(
364 topology,
365 &binding.component_admissions,
366 binding.component_topology_digest,
367 binding.limits.maximum_wasm_store_bytes,
368 union,
369 fixtures,
370 )
371 }
372
373 fn project_for_root(
374 topology: &ComponentTopology,
375 component_admissions: &[canic_core::ids::ComponentSpecAdmission],
376 component_topology_digest: ComponentTopologyDigest,
377 maximum_wasm_store_bytes: u64,
378 union: &ApplicationArtifactUnion,
379 fixtures: &FixtureArtifactManifest,
380 ) -> Result<Self, ApplicationReleaseSetError> {
381 union.validate_against(topology)?;
382 fixtures.validate(topology, union.release_build_id)?;
383 let projected = topology.project_for_admissions(component_admissions)?;
384 let digest = projected.digest()?;
385 if digest != component_topology_digest {
386 return Err(ApplicationReleaseSetError::RootTopologyDigestMismatch {
387 expected: digest,
388 actual: component_topology_digest,
389 });
390 }
391
392 let mut entries = Vec::new();
393 for component_spec in &projected.component_specs {
394 entries.push(project_entry(
395 &component_spec.component_spec,
396 ApplicationReleaseSetEntryKind::Component,
397 &component_spec.component_role,
398 union,
399 )?);
400 for child in &component_spec.children {
401 entries.push(project_entry(
402 &component_spec.component_spec,
403 ApplicationReleaseSetEntryKind::ComponentChild,
404 &child.role,
405 union,
406 )?);
407 }
408 }
409
410 let manifest = Self {
411 release_build_id: union.release_build_id,
412 component_topology_digest: digest,
413 fixtures: project_fixtures(&projected, fixtures),
414 entries,
415 };
416 manifest.validate_for_root(
417 topology,
418 component_admissions,
419 component_topology_digest,
420 maximum_wasm_store_bytes,
421 union,
422 fixtures,
423 )?;
424 Ok(manifest)
425 }
426
427 pub fn validate_against(
429 &self,
430 topology: &ComponentTopology,
431 binding: &FleetSubnetRootBinding,
432 union: &ApplicationArtifactUnion,
433 fixtures: &FixtureArtifactManifest,
434 ) -> Result<(), ApplicationReleaseSetError> {
435 self.validate_for_root(
436 topology,
437 &binding.component_admissions,
438 binding.component_topology_digest,
439 binding.limits.maximum_wasm_store_bytes,
440 union,
441 fixtures,
442 )
443 }
444
445 fn validate_for_root(
446 &self,
447 topology: &ComponentTopology,
448 component_admissions: &[canic_core::ids::ComponentSpecAdmission],
449 component_topology_digest: ComponentTopologyDigest,
450 maximum_wasm_store_bytes: u64,
451 union: &ApplicationArtifactUnion,
452 fixtures: &FixtureArtifactManifest,
453 ) -> Result<(), ApplicationReleaseSetError> {
454 union.validate_against(topology)?;
455 fixtures.validate(topology, union.release_build_id)?;
456 if self.release_build_id != union.release_build_id {
457 return Err(ApplicationReleaseSetError::ManifestBuildMismatch {
458 expected: union.release_build_id,
459 actual: self.release_build_id,
460 });
461 }
462
463 let projected = topology.project_for_admissions(component_admissions)?;
464 let digest = projected.digest()?;
465 if component_topology_digest != digest {
466 return Err(ApplicationReleaseSetError::RootTopologyDigestMismatch {
467 expected: digest,
468 actual: component_topology_digest,
469 });
470 }
471 if self.component_topology_digest != digest {
472 return Err(ApplicationReleaseSetError::ManifestTopologyDigestMismatch {
473 expected: digest,
474 actual: self.component_topology_digest,
475 });
476 }
477
478 let expected = expected_projection_entries(&projected, union)?;
479 if self.entries != expected || self.fixtures != project_fixtures(&projected, fixtures) {
480 return Err(ApplicationReleaseSetError::ProjectionMismatch);
481 }
482
483 let mut unique_artifacts = BTreeSet::new();
484 let total_bytes = self.entries.iter().try_fold(0_u64, |total, entry| {
485 let artifact = &entry.artifact;
486 if unique_artifacts.insert((
487 artifact.wasm_size_bytes,
488 artifact.wasm_sha256_hex.as_str(),
489 artifact.wasm_gz_size_bytes,
490 artifact.wasm_gz_sha256_hex.as_str(),
491 )) {
492 total
493 .checked_add(artifact.wasm_gz_size_bytes)
494 .ok_or(ApplicationReleaseSetError::StoreBytesOverflow)
495 } else {
496 Ok(total)
497 }
498 })?;
499 let mut unique_sources = BTreeSet::new();
500 let total_bytes = self
501 .fixtures
502 .iter()
503 .try_fold(total_bytes, |total, source| {
504 if unique_sources.insert(source.content_id) {
505 total
506 .checked_add(source.descriptor.encoded_length)
507 .ok_or(ApplicationReleaseSetError::StoreBytesOverflow)
508 } else {
509 Ok(total)
510 }
511 })?;
512 if total_bytes > maximum_wasm_store_bytes {
513 return Err(ApplicationReleaseSetError::WasmStoreLimitExceeded {
514 maximum_bytes: maximum_wasm_store_bytes,
515 required_bytes: total_bytes,
516 });
517 }
518
519 Ok(())
520 }
521
522 pub fn canonical_bytes(
524 &self,
525 topology: &ComponentTopology,
526 binding: &FleetSubnetRootBinding,
527 union: &ApplicationArtifactUnion,
528 fixtures: &FixtureArtifactManifest,
529 ) -> Result<Vec<u8>, ApplicationReleaseSetError> {
530 self.validate_against(topology, binding, union, fixtures)?;
531 serde_json::to_vec(self).map_err(ApplicationReleaseSetError::Serialization)
532 }
533
534 pub fn digest(
536 &self,
537 topology: &ComponentTopology,
538 binding: &FleetSubnetRootBinding,
539 union: &ApplicationArtifactUnion,
540 fixtures: &FixtureArtifactManifest,
541 ) -> Result<ReleaseSetDigest, ApplicationReleaseSetError> {
542 Ok(ReleaseSetDigest::from_bytes(
543 Sha256::digest(self.canonical_bytes(topology, binding, union, fixtures)?).into(),
544 ))
545 }
546}
547
548#[derive(Debug, ThisError)]
555pub enum ApplicationReleaseSetError {
556 #[error(transparent)]
557 Fixture(#[from] FixtureArtifactError),
558
559 #[error("application artifact {role} {kind} size cannot be represented")]
560 ArtifactSizeOverflow {
561 role: CanisterRole,
562 kind: &'static str,
563 },
564
565 #[error("application artifact union role set differs from Component Topology")]
566 ArtifactUnionRoleSet {
567 expected: Vec<CanisterRole>,
568 actual: Vec<CanisterRole>,
569 },
570
571 #[error("application build output {role} package {actual} does not match target {expected}")]
572 BuildOutputPackageMismatch {
573 role: CanisterRole,
574 expected: String,
575 actual: String,
576 },
577
578 #[error(
579 "application build output {role} {kind} path {actual} does not match target {expected}"
580 )]
581 BuildOutputPathMismatch {
582 role: CanisterRole,
583 kind: &'static str,
584 expected: String,
585 actual: String,
586 },
587
588 #[error("application build output role set differs from Component Topology")]
589 BuildOutputRoleSet {
590 expected: Vec<CanisterRole>,
591 actual: Vec<CanisterRole>,
592 },
593
594 #[error("application build target role set differs from Component Topology")]
595 BuildTargetRoleSet {
596 expected: Vec<CanisterRole>,
597 actual: Vec<CanisterRole>,
598 },
599
600 #[error("application artifact path is duplicated: {path}")]
601 DuplicateArtifactPath { path: String },
602
603 #[error("application artifact union must not be empty")]
604 EmptyArtifactUnion,
605
606 #[error("application artifact {role} has an empty {kind} payload")]
607 EmptyArtifact {
608 role: CanisterRole,
609 kind: &'static str,
610 },
611
612 #[error("application artifact {role} has invalid gzip Wasm: {source}")]
613 InvalidGzip {
614 role: CanisterRole,
615 #[source]
616 source: std::io::Error,
617 },
618
619 #[error("application artifact {role} has an invalid package identity: {package}")]
620 InvalidPackage { role: CanisterRole, package: String },
621
622 #[error("application artifact {role} has an invalid {kind} path: {path}")]
623 InvalidPath {
624 role: CanisterRole,
625 kind: &'static str,
626 path: String,
627 },
628
629 #[error("application artifact {role} has an invalid {kind} SHA-256: {value}")]
630 InvalidSha256 {
631 role: CanisterRole,
632 kind: &'static str,
633 value: String,
634 },
635
636 #[error("application artifact {role} does not begin with the Wasm magic bytes")]
637 InvalidWasm { role: CanisterRole },
638
639 #[error(
640 "application release-set manifest build {actual} does not match artifact union build {expected}"
641 )]
642 ManifestBuildMismatch {
643 expected: ReleaseBuildId,
644 actual: ReleaseBuildId,
645 },
646
647 #[error("application release-set manifest topology digest differs from its root projection")]
648 ManifestTopologyDigestMismatch {
649 expected: ComponentTopologyDigest,
650 actual: ComponentTopologyDigest,
651 },
652
653 #[error("application artifact union has no build target for role {role}")]
654 MissingBuildTarget { role: CanisterRole },
655
656 #[error("application artifact union has no qualified artifact for role {role}")]
657 MissingArtifact { role: CanisterRole },
658
659 #[error("application artifact order is not canonical: {previous} before {current}")]
660 NonCanonicalArtifactOrder {
661 previous: CanisterRole,
662 current: CanisterRole,
663 },
664
665 #[error("application release-set manifest differs from the exact admitted projection")]
666 ProjectionMismatch,
667
668 #[error(
669 "application artifact {role} release build {actual} does not match union build {expected}"
670 )]
671 ReleaseBuildMismatch {
672 role: CanisterRole,
673 expected: ReleaseBuildId,
674 actual: ReleaseBuildId,
675 },
676
677 #[error("application artifact {role} raw and gzip Wasm representations differ")]
678 RepresentationMismatch { role: CanisterRole },
679
680 #[error("Fleet Subnet Root topology digest differs from its admitted projection")]
681 RootTopologyDigestMismatch {
682 expected: ComponentTopologyDigest,
683 actual: ComponentTopologyDigest,
684 },
685
686 #[error("failed to serialize application release-set evidence: {0}")]
687 Serialization(serde_json::Error),
688
689 #[error("root release-set artifact byte total overflowed")]
690 StoreBytesOverflow,
691
692 #[error(transparent)]
693 Topology(#[from] ComponentTopologyError),
694
695 #[error("application artifact union topology digest differs from Component Topology")]
696 UnionTopologyDigestMismatch {
697 expected: ComponentTopologyDigest,
698 actual: ComponentTopologyDigest,
699 },
700
701 #[error(
702 "root release set requires {required_bytes} Wasm Store bytes, exceeding limit {maximum_bytes}"
703 )]
704 WasmStoreLimitExceeded {
705 maximum_bytes: u64,
706 required_bytes: u64,
707 },
708
709 #[error("application artifact {role} {kind} size must be nonzero")]
710 ZeroSize {
711 role: CanisterRole,
712 kind: &'static str,
713 },
714}
715
716fn topology_roles(topology: &ComponentTopology) -> Vec<CanisterRole> {
717 topology
718 .component_specs
719 .iter()
720 .flat_map(|spec| {
721 std::iter::once(spec.component_role.clone())
722 .chain(spec.children.iter().map(|child| child.role.clone()))
723 })
724 .collect::<BTreeSet<_>>()
725 .into_iter()
726 .collect()
727}
728
729fn compile_entry(
730 release_build_id: ReleaseBuildId,
731 target: &ApplicationArtifactBuildTarget,
732 output: &ApplicationArtifactBuildOutput,
733) -> Result<ApplicationArtifactEntry, ApplicationReleaseSetError> {
734 if output.package != target.package {
735 return Err(ApplicationReleaseSetError::BuildOutputPackageMismatch {
736 role: output.role.clone(),
737 expected: target.package.clone(),
738 actual: output.package.clone(),
739 });
740 }
741 if output.wasm_relative_path != target.wasm_relative_path {
742 return Err(ApplicationReleaseSetError::BuildOutputPathMismatch {
743 role: output.role.clone(),
744 kind: "raw Wasm",
745 expected: target.wasm_relative_path.clone(),
746 actual: output.wasm_relative_path.clone(),
747 });
748 }
749 if output.wasm_gz_relative_path != target.wasm_gz_relative_path {
750 return Err(ApplicationReleaseSetError::BuildOutputPathMismatch {
751 role: output.role.clone(),
752 kind: "gzip Wasm",
753 expected: target.wasm_gz_relative_path.clone(),
754 actual: output.wasm_gz_relative_path.clone(),
755 });
756 }
757 if output.release_build_id != release_build_id {
758 return Err(ApplicationReleaseSetError::ReleaseBuildMismatch {
759 role: output.role.clone(),
760 expected: release_build_id,
761 actual: output.release_build_id,
762 });
763 }
764 let qualified =
765 qualify_representation(&output.wasm, &output.wasm_gz).map_err(|error| match error {
766 RepresentationError::EmptyArtifact { kind } => {
767 ApplicationReleaseSetError::EmptyArtifact {
768 role: output.role.clone(),
769 kind,
770 }
771 }
772 RepresentationError::InvalidWasm => ApplicationReleaseSetError::InvalidWasm {
773 role: output.role.clone(),
774 },
775 RepresentationError::InvalidGzip { source } => {
776 ApplicationReleaseSetError::InvalidGzip {
777 role: output.role.clone(),
778 source,
779 }
780 }
781 RepresentationError::ArtifactSizeOverflow { kind } => {
782 ApplicationReleaseSetError::ArtifactSizeOverflow {
783 role: output.role.clone(),
784 kind,
785 }
786 }
787 RepresentationError::RepresentationMismatch => {
788 ApplicationReleaseSetError::RepresentationMismatch {
789 role: output.role.clone(),
790 }
791 }
792 })?;
793
794 let entry = ApplicationArtifactEntry {
795 role: output.role.clone(),
796 package: output.package.clone(),
797 release_build_id: output.release_build_id,
798 wasm_relative_path: output.wasm_relative_path.clone(),
799 wasm_size_bytes: qualified.wasm_size_bytes,
800 wasm_sha256_hex: qualified.wasm_sha256_hex,
801 wasm_gz_relative_path: output.wasm_gz_relative_path.clone(),
802 wasm_gz_size_bytes: qualified.wasm_gz_size_bytes,
803 wasm_gz_sha256_hex: qualified.wasm_gz_sha256_hex,
804 candid_sha256: output.candid_sha256,
805 protocol_profile_digest: output.protocol_profile_digest,
806 };
807 validate_entry(release_build_id, &entry)?;
808 Ok(entry)
809}
810
811fn validate_entry(
812 release_build_id: ReleaseBuildId,
813 entry: &ApplicationArtifactEntry,
814) -> Result<(), ApplicationReleaseSetError> {
815 if entry.release_build_id != release_build_id {
816 return Err(ApplicationReleaseSetError::ReleaseBuildMismatch {
817 role: entry.role.clone(),
818 expected: release_build_id,
819 actual: entry.release_build_id,
820 });
821 }
822 if !valid_package_name(&entry.package) {
823 return Err(ApplicationReleaseSetError::InvalidPackage {
824 role: entry.role.clone(),
825 package: entry.package.clone(),
826 });
827 }
828 validate_path(&entry.role, "raw Wasm", &entry.wasm_relative_path)?;
829 validate_path(&entry.role, "gzip Wasm", &entry.wasm_gz_relative_path)?;
830 if entry.wasm_size_bytes == 0 {
831 return Err(ApplicationReleaseSetError::ZeroSize {
832 role: entry.role.clone(),
833 kind: "raw Wasm",
834 });
835 }
836 if entry.wasm_gz_size_bytes == 0 {
837 return Err(ApplicationReleaseSetError::ZeroSize {
838 role: entry.role.clone(),
839 kind: "gzip Wasm",
840 });
841 }
842 validate_sha256(&entry.role, "raw Wasm", &entry.wasm_sha256_hex)?;
843 validate_sha256(&entry.role, "gzip Wasm", &entry.wasm_gz_sha256_hex)
844}
845
846fn validate_path(
847 role: &CanisterRole,
848 kind: &'static str,
849 path: &str,
850) -> Result<(), ApplicationReleaseSetError> {
851 if path.len() > MAX_ARTIFACT_PATH_BYTES
852 || validate_release_artifact_relative_path(path).is_err()
853 {
854 return Err(ApplicationReleaseSetError::InvalidPath {
855 role: role.clone(),
856 kind,
857 path: path.to_string(),
858 });
859 }
860 Ok(())
861}
862
863fn validate_sha256(
864 role: &CanisterRole,
865 kind: &'static str,
866 value: &str,
867) -> Result<(), ApplicationReleaseSetError> {
868 let canonical = value.len() == SHA_256_HEX_BYTES
869 && value
870 .bytes()
871 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
872 && decode_hex(value).is_ok();
873 if canonical {
874 Ok(())
875 } else {
876 Err(ApplicationReleaseSetError::InvalidSha256 {
877 role: role.clone(),
878 kind,
879 value: value.to_string(),
880 })
881 }
882}
883
884fn project_entry(
885 component_spec: &ComponentSpecId,
886 kind: ApplicationReleaseSetEntryKind,
887 role: &CanisterRole,
888 union: &ApplicationArtifactUnion,
889) -> Result<FleetSubnetRootReleaseSetEntry, ApplicationReleaseSetError> {
890 let artifact = union
891 .get(role)
892 .ok_or_else(|| ApplicationReleaseSetError::MissingArtifact { role: role.clone() })?;
893 Ok(FleetSubnetRootReleaseSetEntry {
894 component_spec: component_spec.clone(),
895 kind,
896 artifact: artifact.clone(),
897 })
898}
899
900fn expected_projection_entries(
901 topology: &ComponentTopology,
902 union: &ApplicationArtifactUnion,
903) -> Result<Vec<FleetSubnetRootReleaseSetEntry>, ApplicationReleaseSetError> {
904 let mut entries = Vec::new();
905 for component_spec in &topology.component_specs {
906 entries.push(project_entry(
907 &component_spec.component_spec,
908 ApplicationReleaseSetEntryKind::Component,
909 &component_spec.component_role,
910 union,
911 )?);
912 for child in &component_spec.children {
913 entries.push(project_entry(
914 &component_spec.component_spec,
915 ApplicationReleaseSetEntryKind::ComponentChild,
916 &child.role,
917 union,
918 )?);
919 }
920 }
921 Ok(entries)
922}
923
924fn project_fixtures(
925 topology: &ComponentTopology,
926 fixtures: &FixtureArtifactManifest,
927) -> Vec<canic_core::dto::root_store::RootStoreFixture> {
928 let roles = topology_roles(topology);
929 fixtures
930 .entries
931 .iter()
932 .filter(|entry| roles.contains(&entry.role))
933 .map(|entry| canic_core::dto::root_store::RootStoreFixture {
934 role: entry.role.clone(),
935 content_id: entry.content_id,
936 descriptor: entry.descriptor.clone(),
937 })
938 .collect()
939}