1#[cfg(test)]
8mod tests;
9
10use super::{
11 CanicInfrastructureArtifactInput, CanicInfrastructureArtifactManifest,
12 CanicInfrastructureArtifactManifestError, CanicInfrastructureRole,
13};
14use crate::{
15 MAX_DOCUMENT_READ_BYTES,
16 release_build::{ReleaseBuildPlanError, ReleaseBuildPlanState, load_release_build_plan},
17 release_set::artifact::{
18 ReleaseArtifactMaterializationError, contains_release_build_identity,
19 materialize_qualified_release_artifact,
20 },
21};
22use canic_contracts::ids::{CanisterRole, ReleaseBuildId};
23use canic_core::role_contract::{ProtocolProfileDigest, RoleCapabilityKey};
24use ic_host_artifacts::artifact::ArtifactError;
25use ic_host_fs::{durable::create_new_bytes_with_parents, read::read_optional_file_no_follow};
26use sha2_host::{Digest, Sha256};
27use std::{
28 collections::BTreeSet,
29 io,
30 path::{Path, PathBuf},
31};
32use thiserror::Error as ThisError;
33
34pub const INFRASTRUCTURE_ARTIFACT_MANIFEST_FILE: &str = "infrastructure-artifact-manifest.json";
35
36#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct PersistedCanicInfrastructureArtifactManifest {
44 pub manifest: CanicInfrastructureArtifactManifest,
45 pub digest: [u8; 32],
46 pub path: PathBuf,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct CanicInfrastructureArtifactBuildOutput {
57 pub role: CanicInfrastructureRole,
58 pub package: String,
59 pub protocol_release_identity: String,
60 pub protocol_role: CanisterRole,
61 pub protocol_capabilities: BTreeSet<RoleCapabilityKey>,
62 pub release_build_id: ReleaseBuildId,
63 pub wasm_path: PathBuf,
64 pub wasm_gz_path: PathBuf,
65 pub candid_sha256: [u8; 32],
66 pub protocol_profile_digest: ProtocolProfileDigest,
67}
68
69#[derive(Debug, ThisError)]
76pub enum CanicInfrastructureArtifactPersistenceError {
77 #[error("infrastructure artifact {role:?} {kind} path is outside the ICP root: {path}")]
78 ArtifactOutsideRoot {
79 role: CanicInfrastructureRole,
80 kind: &'static str,
81 path: PathBuf,
82 },
83
84 #[error("failed to read infrastructure artifact {role:?} {kind} at {path}: {source}")]
85 ArtifactRead {
86 role: CanicInfrastructureRole,
87 kind: &'static str,
88 path: PathBuf,
89 #[source]
90 source: io::Error,
91 },
92
93 #[error(
94 "infrastructure artifact manifest already exists with different canonical bytes: {path}"
95 )]
96 ConflictingManifest { path: PathBuf },
97
98 #[error(
99 "finalized release build {release_build_id} has no exact infrastructure artifact manifest"
100 )]
101 FinalizedWithoutExactManifest { release_build_id: ReleaseBuildId },
102
103 #[error("infrastructure artifact {role:?} has an invalid {kind} path: {path}")]
104 InvalidArtifactPath {
105 role: CanicInfrastructureRole,
106 kind: &'static str,
107 path: PathBuf,
108 },
109
110 #[error(
111 "infrastructure artifact {role:?} raw Wasm at {path} does not embed release build {release_build_id}"
112 )]
113 MissingReleaseBuildIdentity {
114 role: CanicInfrastructureRole,
115 release_build_id: ReleaseBuildId,
116 path: PathBuf,
117 },
118
119 #[error("invalid infrastructure artifact manifest {path}: {reason}")]
120 InvalidManifestDocument { path: PathBuf, reason: String },
121
122 #[error(transparent)]
123 Manifest(#[from] CanicInfrastructureArtifactManifestError),
124
125 #[error("failed to access infrastructure artifact manifest {path}: {source}")]
126 ManifestIo {
127 path: PathBuf,
128 #[source]
129 source: io::Error,
130 },
131
132 #[error("infrastructure artifact manifest is missing: {path}")]
133 MissingManifest { path: PathBuf },
134
135 #[error("infrastructure artifact {role:?} has a non-UTF-8 {kind} path: {path}")]
136 NonUtf8ArtifactPath {
137 role: CanicInfrastructureRole,
138 kind: &'static str,
139 path: PathBuf,
140 },
141
142 #[error(transparent)]
143 ReleaseBuild(#[from] ReleaseBuildPlanError),
144
145 #[error("infrastructure artifact {role:?} {kind} is not a regular no-follow file: {path}")]
146 UnsafeArtifact {
147 role: CanicInfrastructureRole,
148 kind: &'static str,
149 path: PathBuf,
150 },
151
152 #[error("infrastructure artifact manifest is not a regular no-follow file: {path}")]
153 UnsafeManifest { path: PathBuf },
154}
155
156struct MaterializedBuildOutput {
157 role: CanicInfrastructureRole,
158 package: String,
159 protocol_release_identity: String,
160 protocol_role: CanisterRole,
161 protocol_capabilities: BTreeSet<RoleCapabilityKey>,
162 release_build_id: ReleaseBuildId,
163 wasm_relative_path: String,
164 wasm: Vec<u8>,
165 wasm_gz_relative_path: String,
166 wasm_gz: Vec<u8>,
167 candid_sha256: [u8; 32],
168 protocol_profile_digest: ProtocolProfileDigest,
169}
170
171impl MaterializedBuildOutput {
172 fn as_input(&self) -> CanicInfrastructureArtifactInput<'_> {
173 CanicInfrastructureArtifactInput {
174 role: self.role,
175 package: &self.package,
176 protocol_release_identity: &self.protocol_release_identity,
177 protocol_role: &self.protocol_role,
178 protocol_capabilities: &self.protocol_capabilities,
179 release_build_id: self.release_build_id,
180 wasm_relative_path: &self.wasm_relative_path,
181 wasm: &self.wasm,
182 wasm_gz_relative_path: &self.wasm_gz_relative_path,
183 wasm_gz: &self.wasm_gz,
184 candid_sha256: self.candid_sha256,
185 protocol_profile_digest: self.protocol_profile_digest,
186 }
187 }
188}
189
190pub fn compile_and_persist_canic_infrastructure_artifact_manifest(
192 root: &Path,
193 release_build_id: ReleaseBuildId,
194 outputs: &[CanicInfrastructureArtifactBuildOutput],
195) -> Result<PersistedCanicInfrastructureArtifactManifest, CanicInfrastructureArtifactPersistenceError>
196{
197 let release_build = load_release_build_plan(root, release_build_id)?;
198 let materialized = outputs
199 .iter()
200 .map(|output| materialize_build_output(root, release_build_id, output))
201 .collect::<Result<Vec<_>, _>>()?;
202 let inputs = materialized
203 .iter()
204 .map(MaterializedBuildOutput::as_input)
205 .collect::<Vec<_>>();
206 let manifest = CanicInfrastructureArtifactManifest::compile(release_build_id, &inputs)?;
207 let expected = persisted_manifest(root, manifest)?;
208 let existing = load_optional_persisted_manifest(&expected.path, release_build_id)?;
209
210 if matches!(release_build.state, ReleaseBuildPlanState::Finalized { .. }) {
211 return match existing {
212 Some(observed) if observed.manifest == expected.manifest => Ok(observed),
213 _ => Err(
214 CanicInfrastructureArtifactPersistenceError::FinalizedWithoutExactManifest {
215 release_build_id,
216 },
217 ),
218 };
219 }
220
221 if let Some(observed) = existing {
222 return if observed.manifest == expected.manifest {
223 Ok(observed)
224 } else {
225 Err(
226 CanicInfrastructureArtifactPersistenceError::ConflictingManifest {
227 path: expected.path,
228 },
229 )
230 };
231 }
232
233 let canonical_bytes = expected.manifest.canonical_bytes()?;
234 if let Err(source) = create_new_bytes_with_parents(&expected.path, &canonical_bytes)
235 .map_err(crate::publication::ops::io_error)
236 {
237 match load_optional_persisted_manifest(&expected.path, release_build_id) {
238 Ok(Some(observed)) if observed.manifest == expected.manifest => return Ok(observed),
239 Ok(Some(_)) if source.kind() == io::ErrorKind::AlreadyExists => {
240 return Err(
241 CanicInfrastructureArtifactPersistenceError::ConflictingManifest {
242 path: expected.path,
243 },
244 );
245 }
246 _ => {
247 return Err(CanicInfrastructureArtifactPersistenceError::ManifestIo {
248 path: expected.path,
249 source,
250 });
251 }
252 }
253 }
254
255 load_persisted_canic_infrastructure_artifact_manifest(root, release_build_id)
256}
257
258pub fn load_persisted_canic_infrastructure_artifact_manifest(
260 root: &Path,
261 release_build_id: ReleaseBuildId,
262) -> Result<PersistedCanicInfrastructureArtifactManifest, CanicInfrastructureArtifactPersistenceError>
263{
264 let path = infrastructure_artifact_manifest_path(root, release_build_id);
265 load_optional_persisted_manifest(&path, release_build_id)?
266 .ok_or(CanicInfrastructureArtifactPersistenceError::MissingManifest { path })
267}
268
269fn materialize_build_output(
270 root: &Path,
271 release_build_id: ReleaseBuildId,
272 output: &CanicInfrastructureArtifactBuildOutput,
273) -> Result<MaterializedBuildOutput, CanicInfrastructureArtifactPersistenceError> {
274 let role = output.role;
275 if output.release_build_id != release_build_id {
276 return Err(
277 CanicInfrastructureArtifactManifestError::ReleaseBuildMismatch {
278 role,
279 expected: release_build_id,
280 actual: output.release_build_id,
281 }
282 .into(),
283 );
284 }
285
286 let wasm = materialize_artifact(root, role, "raw Wasm", &output.wasm_path)?;
287 if !contains_release_build_identity(&wasm.bytes, release_build_id) {
288 return Err(
289 CanicInfrastructureArtifactPersistenceError::MissingReleaseBuildIdentity {
290 role,
291 release_build_id,
292 path: output.wasm_path.clone(),
293 },
294 );
295 }
296 let wasm_gz = materialize_artifact(root, role, "gzip Wasm", &output.wasm_gz_path)?;
297
298 Ok(MaterializedBuildOutput {
299 role,
300 package: output.package.clone(),
301 protocol_release_identity: output.protocol_release_identity.clone(),
302 protocol_role: output.protocol_role.clone(),
303 protocol_capabilities: output.protocol_capabilities.clone(),
304 release_build_id: output.release_build_id,
305 wasm_relative_path: wasm.relative_path,
306 wasm: wasm.bytes,
307 wasm_gz_relative_path: wasm_gz.relative_path,
308 wasm_gz: wasm_gz.bytes,
309 candid_sha256: output.candid_sha256,
310 protocol_profile_digest: output.protocol_profile_digest,
311 })
312}
313
314fn materialize_artifact(
315 root: &Path,
316 role: CanicInfrastructureRole,
317 kind: &'static str,
318 path: &Path,
319) -> Result<
320 crate::release_set::artifact::MaterializedReleaseArtifact,
321 CanicInfrastructureArtifactPersistenceError,
322> {
323 materialize_qualified_release_artifact(root, path).map_err(|error| match error {
324 ReleaseArtifactMaterializationError::InvalidPath => {
325 CanicInfrastructureArtifactPersistenceError::InvalidArtifactPath {
326 role,
327 kind,
328 path: path.to_path_buf(),
329 }
330 }
331 ReleaseArtifactMaterializationError::NonUtf8Path => {
332 CanicInfrastructureArtifactPersistenceError::NonUtf8ArtifactPath {
333 role,
334 kind,
335 path: path.to_path_buf(),
336 }
337 }
338 ReleaseArtifactMaterializationError::OutsideRoot => {
339 CanicInfrastructureArtifactPersistenceError::ArtifactOutsideRoot {
340 role,
341 kind,
342 path: path.to_path_buf(),
343 }
344 }
345 ReleaseArtifactMaterializationError::Read(source) => {
346 CanicInfrastructureArtifactPersistenceError::ArtifactRead {
347 role,
348 kind,
349 path: path.to_path_buf(),
350 source,
351 }
352 }
353 ReleaseArtifactMaterializationError::UnsafeFile => {
354 CanicInfrastructureArtifactPersistenceError::UnsafeArtifact {
355 role,
356 kind,
357 path: path.to_path_buf(),
358 }
359 }
360 })
361}
362
363fn persisted_manifest(
364 root: &Path,
365 manifest: CanicInfrastructureArtifactManifest,
366) -> Result<PersistedCanicInfrastructureArtifactManifest, CanicInfrastructureArtifactPersistenceError>
367{
368 let digest = manifest.digest()?;
369 let path = infrastructure_artifact_manifest_path(root, manifest.release_build_id);
370 Ok(PersistedCanicInfrastructureArtifactManifest {
371 manifest,
372 digest,
373 path,
374 })
375}
376
377fn load_optional_persisted_manifest(
378 path: &Path,
379 release_build_id: ReleaseBuildId,
380) -> Result<
381 Option<PersistedCanicInfrastructureArtifactManifest>,
382 CanicInfrastructureArtifactPersistenceError,
383> {
384 let bytes = match read_optional_file_no_follow(path, MAX_DOCUMENT_READ_BYTES) {
385 Ok(bytes) => bytes,
386 Err(ArtifactError::NotRegularFile) => {
387 return Err(
388 CanicInfrastructureArtifactPersistenceError::UnsafeManifest {
389 path: path.to_path_buf(),
390 },
391 );
392 }
393 Err(source) => {
394 return Err(CanicInfrastructureArtifactPersistenceError::ManifestIo {
395 path: path.to_path_buf(),
396 source: source.into(),
397 });
398 }
399 };
400 let Some(bytes) = bytes else {
401 return Ok(None);
402 };
403
404 let manifest: CanicInfrastructureArtifactManifest =
405 serde_json::from_slice(&bytes).map_err(|error| {
406 CanicInfrastructureArtifactPersistenceError::InvalidManifestDocument {
407 path: path.to_path_buf(),
408 reason: error.to_string(),
409 }
410 })?;
411 if manifest.release_build_id != release_build_id {
412 return Err(
413 CanicInfrastructureArtifactPersistenceError::InvalidManifestDocument {
414 path: path.to_path_buf(),
415 reason: format!(
416 "manifest release build {} does not match path release build {release_build_id}",
417 manifest.release_build_id
418 ),
419 },
420 );
421 }
422 let canonical = manifest.canonical_bytes()?;
423 if canonical != bytes {
424 return Err(
425 CanicInfrastructureArtifactPersistenceError::InvalidManifestDocument {
426 path: path.to_path_buf(),
427 reason: "manifest bytes are not canonical".to_string(),
428 },
429 );
430 }
431
432 Ok(Some(PersistedCanicInfrastructureArtifactManifest {
433 digest: Sha256::digest(&bytes).into(),
434 manifest,
435 path: path.to_path_buf(),
436 }))
437}
438
439fn infrastructure_artifact_manifest_path(root: &Path, release_build_id: ReleaseBuildId) -> PathBuf {
440 root.join(".canic")
441 .join("release-builds")
442 .join(release_build_id.to_string())
443 .join(INFRASTRUCTURE_ARTIFACT_MANIFEST_FILE)
444}