Skip to main content

canic_core/api/runtime/
install.rs

1use crate::{
2    InternalError, InternalErrorOrigin,
3    cdk::{types::Principal, utils::wasm::get_wasm_hash},
4    dto::error::Error,
5    format::byte_size,
6    ids::CanisterRole,
7};
8use async_trait::async_trait;
9use std::{
10    borrow::Cow,
11    collections::BTreeMap,
12    sync::{Mutex, OnceLock},
13};
14
15///
16/// ApprovedModulePayload
17///
18
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub enum ApprovedModulePayload {
21    Chunked {
22        source_canister: Principal,
23        chunk_hashes: Vec<Vec<u8>>,
24    },
25    Embedded {
26        wasm_module: Cow<'static, [u8]>,
27    },
28}
29
30///
31/// ApprovedModuleSource
32///
33
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct ApprovedModuleSource {
36    source_label: String,
37    module_hash: Vec<u8>,
38    payload_size_bytes: u64,
39    payload: ApprovedModulePayload,
40}
41
42impl ApprovedModuleSource {
43    /// Construct one chunk-store-backed module source.
44    #[must_use]
45    pub const fn chunked(
46        source_canister: Principal,
47        source_label: String,
48        module_hash: Vec<u8>,
49        chunk_hashes: Vec<Vec<u8>>,
50        payload_size_bytes: u64,
51    ) -> Self {
52        Self {
53            source_label,
54            module_hash,
55            payload_size_bytes,
56            payload: ApprovedModulePayload::Chunked {
57                source_canister,
58                chunk_hashes,
59            },
60        }
61    }
62
63    /// Construct one embedded module source from already packaged wasm bytes.
64    #[must_use]
65    pub fn embedded(source_label: String, wasm_module: &'static [u8]) -> Self {
66        let payload_size_bytes = wasm_module.len() as u64;
67
68        Self {
69            source_label,
70            module_hash: get_wasm_hash(wasm_module),
71            payload_size_bytes,
72            payload: ApprovedModulePayload::Embedded {
73                wasm_module: Cow::Borrowed(wasm_module),
74            },
75        }
76    }
77
78    /// Return the logical source label used for logs and status output.
79    #[must_use]
80    pub fn source_label(&self) -> &str {
81        &self.source_label
82    }
83
84    /// Return the installable wasm module hash.
85    #[must_use]
86    pub fn module_hash(&self) -> &[u8] {
87        &self.module_hash
88    }
89
90    /// Return the formatted module payload size for logs and status output.
91    #[must_use]
92    pub fn payload_size(&self) -> String {
93        byte_size(self.payload_size_bytes)
94    }
95
96    /// Return the raw payload size in bytes.
97    #[must_use]
98    pub const fn payload_size_bytes(&self) -> u64 {
99        self.payload_size_bytes
100    }
101
102    /// Return the chunk count when the source is chunk-store-backed.
103    #[must_use]
104    pub const fn chunk_count(&self) -> usize {
105        match &self.payload {
106            ApprovedModulePayload::Chunked { chunk_hashes, .. } => chunk_hashes.len(),
107            ApprovedModulePayload::Embedded { .. } => 0,
108        }
109    }
110
111    /// Return the underlying payload representation.
112    #[must_use]
113    pub const fn payload(&self) -> &ApprovedModulePayload {
114        &self.payload
115    }
116}
117
118///
119/// ModuleSourceResolver
120///
121
122#[async_trait]
123pub trait ModuleSourceResolver: Send + Sync {
124    /// Resolve the currently approved install source for one canister role.
125    async fn approved_module_source(
126        &self,
127        role: &CanisterRole,
128    ) -> Result<ApprovedModuleSource, Error>;
129}
130
131static MODULE_SOURCE_RESOLVER: OnceLock<&'static dyn ModuleSourceResolver> = OnceLock::new();
132static EMBEDDED_MODULE_SOURCES: OnceLock<Mutex<BTreeMap<CanisterRole, ApprovedModuleSource>>> =
133    OnceLock::new();
134
135///
136/// ModuleSourceRuntimeApi
137///
138
139pub struct ModuleSourceRuntimeApi;
140
141impl ModuleSourceRuntimeApi {
142    /// Register one built-in module source override for the current process.
143    pub fn register_embedded_module_source(role: CanisterRole, source: ApprovedModuleSource) {
144        let sources = EMBEDDED_MODULE_SOURCES.get_or_init(|| Mutex::new(BTreeMap::new()));
145        let mut sources = sources
146            .lock()
147            .unwrap_or_else(std::sync::PoisonError::into_inner);
148
149        match sources.get(&role) {
150            Some(existing) if existing == &source => {}
151            Some(existing) => {
152                panic!(
153                    "embedded module source for role '{role}' was already registered with a different payload: existing='{}' new='{}'",
154                    existing.source_label(),
155                    source.source_label()
156                );
157            }
158            None => {
159                sources.insert(role, source);
160            }
161        }
162    }
163
164    /// Register one embedded wasm payload as the built-in install source for one role.
165    pub fn register_embedded_module_wasm(
166        role: CanisterRole,
167        source_label: impl Into<String>,
168        wasm_module: &'static [u8],
169    ) {
170        Self::register_embedded_module_source(
171            role,
172            ApprovedModuleSource::embedded(source_label.into(), wasm_module),
173        );
174    }
175
176    /// Register the control-plane resolver used by root-owned installation flows.
177    pub fn register_module_source_resolver(resolver: &'static dyn ModuleSourceResolver) {
178        let _ = MODULE_SOURCE_RESOLVER.set(resolver);
179    }
180
181    /// Return whether one embedded module source override has been registered.
182    #[must_use]
183    pub fn has_embedded_module_source(role: &CanisterRole) -> bool {
184        EMBEDDED_MODULE_SOURCES.get().is_some_and(|sources| {
185            let sources = sources
186                .lock()
187                .unwrap_or_else(std::sync::PoisonError::into_inner);
188            sources.contains_key(role)
189        })
190    }
191
192    /// Resolve the approved install source for one canister role through the registered driver.
193    pub(crate) async fn approved_module_source(
194        role: &CanisterRole,
195    ) -> Result<ApprovedModuleSource, InternalError> {
196        if let Some(source) = EMBEDDED_MODULE_SOURCES.get().and_then(|sources| {
197            let sources = sources
198                .lock()
199                .unwrap_or_else(std::sync::PoisonError::into_inner);
200            sources.get(role).cloned()
201        }) {
202            return Ok(source);
203        }
204
205        let resolver = MODULE_SOURCE_RESOLVER.get().ok_or_else(|| {
206            InternalError::workflow(
207                InternalErrorOrigin::Workflow,
208                "module source resolver is not registered; root/control-plane install flows are unavailable".to_string(),
209            )
210        })?;
211
212        resolver
213            .approved_module_source(role)
214            .await
215            .map_err(InternalError::public)
216    }
217}