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