Skip to main content

canic_host/release_set/candid/
mod.rs

1//! Resolve an offline declaration through exact finalized release authority.
2//!
3//! This owner verifies retained manifest and declaration bytes; it does not select
4//! the latest build, require current application source, or contact a canister.
5
6#[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///
26/// BuiltCandid
27///
28/// Exact declaration bytes verified by the managed artifact owner for offline inspection.
29///
30
31#[derive(Debug)]
32pub struct BuiltCandid {
33    pub release_build_id: ReleaseBuildId,
34    pub path: PathBuf,
35    pub candid: String,
36}
37
38///
39/// BuiltCandidError
40///
41/// Failure to bind an offline declaration to a finalized managed build.
42///
43
44#[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
84/// Load one role's exact declaration without live lookup or current-source substitution.
85pub 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}