canic_core/api/runtime/
install.rs1use 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#[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#[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 #[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 #[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 #[must_use]
80 pub fn source_label(&self) -> &str {
81 &self.source_label
82 }
83
84 #[must_use]
86 pub fn module_hash(&self) -> &[u8] {
87 &self.module_hash
88 }
89
90 #[must_use]
92 pub fn payload_size(&self) -> String {
93 byte_size(self.payload_size_bytes)
94 }
95
96 #[must_use]
98 pub const fn payload_size_bytes(&self) -> u64 {
99 self.payload_size_bytes
100 }
101
102 #[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 #[must_use]
113 pub const fn payload(&self) -> &ApprovedModulePayload {
114 &self.payload
115 }
116}
117
118#[async_trait]
123pub trait ModuleSourceResolver: Send + Sync {
124 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
135pub struct ModuleSourceRuntimeApi;
140
141impl ModuleSourceRuntimeApi {
142 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 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 pub fn register_module_source_resolver(resolver: &'static dyn ModuleSourceResolver) {
178 let _ = MODULE_SOURCE_RESOLVER.set(resolver);
179 }
180
181 #[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 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}