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