canic_host/release_set/candid/
mod.rs1#[cfg(test)]
7mod tests;
8
9use crate::{
10 durable_io::{RegularFileReadError, read_optional_regular_bytes},
11 release_build::{ReleaseBuildPlanError, validate_finalized_release_build_manifest},
12 release_set::{
13 ApplicationArtifactUnionPersistenceError, CanicInfrastructureArtifactPersistenceError,
14 CurrentReleaseSetManifestError, application::load_retained_application_artifact_union,
15 load_persisted_canic_infrastructure_artifact_manifest,
16 load_persisted_current_release_set_manifest,
17 },
18};
19use std::path::{Path, PathBuf};
20
21use canic_core::ids::ReleaseBuildId;
22use sha2_host::{Digest, Sha256};
23use thiserror::Error;
24
25#[derive(Debug)]
32pub struct BuiltCandid {
33 pub release_build_id: ReleaseBuildId,
34 pub path: PathBuf,
35 pub candid: String,
36}
37
38#[derive(Debug, Error)]
45pub enum BuiltCandidError {
46 #[error("selected build contains ambiguous role {0}")]
47 AmbiguousRole(String),
48
49 #[error(transparent)]
50 Application(#[from] ApplicationArtifactUnionPersistenceError),
51
52 #[error("selected build declaration hash differs: {}", .0.display())]
53 CandidDigestMismatch(PathBuf),
54
55 #[error(transparent)]
56 Infrastructure(#[from] CanicInfrastructureArtifactPersistenceError),
57
58 #[error("selected build declaration is not UTF-8: {}", .0.display())]
59 InvalidUtf8(PathBuf),
60
61 #[error("selected build child manifest digest differs from finalized authority")]
62 ManifestDigestMismatch,
63
64 #[error("selected build has no role {0}")]
65 MissingRole(String),
66
67 #[error("failed to read selected build declaration {}: {source}", path.display())]
68 Read {
69 path: PathBuf,
70 #[source]
71 source: std::io::Error,
72 },
73
74 #[error(transparent)]
75 ReleaseBuild(#[from] ReleaseBuildPlanError),
76
77 #[error(transparent)]
78 ReleaseSet(#[from] CurrentReleaseSetManifestError),
79
80 #[error("selected build declaration is missing or not a regular contained file: {}", .0.display())]
81 UnsafeCandid(PathBuf),
82}
83
84pub fn load_built_candid(
86 root: &Path,
87 release_build_id: ReleaseBuildId,
88 role: &str,
89) -> Result<BuiltCandid, BuiltCandidError> {
90 let release = load_persisted_current_release_set_manifest(root, release_build_id)?;
91 validate_finalized_release_build_manifest(root, release_build_id, &release.path)?;
92 let application = load_retained_application_artifact_union(root, release_build_id)?;
93 let infrastructure =
94 load_persisted_canic_infrastructure_artifact_manifest(root, release_build_id)?;
95 if application.digest != release.manifest.application_artifact_union_sha256
96 || infrastructure.digest != release.manifest.infrastructure_artifact_manifest_sha256
97 {
98 return Err(BuiltCandidError::ManifestDigestMismatch);
99 }
100 let mut matches = application
101 .union
102 .entries
103 .iter()
104 .filter(|entry| entry.role.as_str() == role)
105 .map(|entry| (&entry.wasm_relative_path, entry.candid_sha256))
106 .chain(
107 infrastructure
108 .manifest
109 .entries
110 .iter()
111 .filter(|entry| entry.protocol_role.as_str() == role)
112 .map(|entry| (&entry.wasm_relative_path, entry.candid_sha256)),
113 );
114 let (wasm, digest) = matches
115 .next()
116 .ok_or_else(|| BuiltCandidError::MissingRole(role.into()))?;
117 if matches.next().is_some() {
118 return Err(BuiltCandidError::AmbiguousRole(role.into()));
119 }
120 let path = root.join(wasm).with_extension("did");
121 let bytes = read_contained_candid(root, &path)?;
122 let observed: [u8; 32] = Sha256::digest(&bytes).into();
123 if observed != digest {
124 return Err(BuiltCandidError::CandidDigestMismatch(path));
125 }
126 let candid =
127 String::from_utf8(bytes).map_err(|_| BuiltCandidError::InvalidUtf8(path.clone()))?;
128 Ok(BuiltCandid {
129 release_build_id,
130 path,
131 candid,
132 })
133}
134
135fn read_contained_candid(root: &Path, path: &Path) -> Result<Vec<u8>, BuiltCandidError> {
136 let unsafe_path = || BuiltCandidError::UnsafeCandid(path.to_path_buf());
137 let canonical_root = root.canonicalize().map_err(|_| unsafe_path())?;
138 let parent = path.parent().ok_or_else(unsafe_path)?;
139 if !parent
140 .canonicalize()
141 .map_err(|_| unsafe_path())?
142 .starts_with(canonical_root)
143 {
144 return Err(unsafe_path());
145 }
146 match read_optional_regular_bytes(path) {
147 Ok(Some(bytes)) => Ok(bytes),
148 Err(RegularFileReadError::Io(source)) => Err(BuiltCandidError::Read {
149 path: path.to_path_buf(),
150 source,
151 }),
152 _ => Err(unsafe_path()),
153 }
154}