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