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