Skip to main content

canic_host/release_set/infrastructure/persistence/
mod.rs

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