1#[cfg(test)]
8mod tests;
9
10use crate::node::{
11 stable_memory_key, validate_app_memory_id, validate_memory_id_in_range,
12 validate_memory_id_not_reserved, validate_stable_key, validate_stable_key_segment,
13};
14use crate::prelude::*;
15use std::collections::BTreeMap;
16use std::str::FromStr;
17
18pub type MigrationPlanConstructor =
20 fn() -> Result<icydb_schema::SchemaMigrationPlan, icydb_schema::SchemaContractError>;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "snake_case")]
32pub enum CanisterMemoryProfile {
33 Compact,
35 General,
37 HighHeadroom,
39}
40
41impl CanisterMemoryProfile {
42 #[must_use]
44 pub const fn bucket_size_pages(self) -> u16 {
45 match self {
46 Self::Compact => 4,
47 Self::General => 16,
48 Self::HighHeadroom => 128,
49 }
50 }
51}
52
53#[doc(hidden)]
55pub fn migration_literal_from_text(
56 kind: &str,
57 value: &str,
58) -> Result<icydb_schema::ScalarLiteral, icydb_schema::SchemaContractError> {
59 use icydb_schema::{
60 Account, Blob, Date, Decimal, Duration, IntBig, NatBig, Principal, ScalarLiteral,
61 Subaccount, Timestamp, U256, Ulid,
62 };
63
64 let invalid = || icydb_schema::SchemaContractError::InvalidLiteral;
65 match kind {
66 "account" => Account::from_str(value)
67 .map(ScalarLiteral::Account)
68 .map_err(|_| invalid()),
69 "blob" => decode_migration_hex(value)
70 .map(Blob::from)
71 .map(ScalarLiteral::Blob),
72 "date" => Date::parse(value)
73 .map(ScalarLiteral::Date)
74 .ok_or_else(invalid),
75 "decimal" => Decimal::from_str(value)
76 .map(ScalarLiteral::Decimal)
77 .map_err(|_| invalid()),
78 "duration" => Duration::parse_flexible(value)
79 .map(ScalarLiteral::Duration)
80 .map_err(|_| invalid()),
81 "int_big" => IntBig::from_str(value)
82 .map(ScalarLiteral::IntBig)
83 .map_err(|_| invalid()),
84 "nat_big" => NatBig::from_str(value)
85 .map(ScalarLiteral::NatBig)
86 .map_err(|_| invalid()),
87 "principal" => Principal::from_str(value)
88 .map(ScalarLiteral::Principal)
89 .map_err(|_| invalid()),
90 "subaccount" => {
91 let bytes = decode_migration_hex(value)?;
92 let bytes: [u8; 32] = bytes.try_into().map_err(|_| invalid())?;
93 Ok(ScalarLiteral::Subaccount(Subaccount::from_array(bytes)))
94 }
95 "timestamp" => Timestamp::parse_flexible(value)
96 .map(ScalarLiteral::Timestamp)
97 .map_err(|_| invalid()),
98 "u256" => U256::from_str(value)
99 .map(ScalarLiteral::U256)
100 .map_err(|_| invalid()),
101 "ulid" => Ulid::from_str(value)
102 .map(ScalarLiteral::Ulid)
103 .map_err(|_| invalid()),
104 _ => Err(invalid()),
105 }
106}
107
108fn decode_migration_hex(value: &str) -> Result<Vec<u8>, icydb_schema::SchemaContractError> {
109 if !value.len().is_multiple_of(2) {
110 return Err(icydb_schema::SchemaContractError::InvalidLiteral);
111 }
112 value
113 .as_bytes()
114 .as_chunks::<2>()
115 .0
116 .iter()
117 .map(|pair| {
118 let high = decode_hex_nibble(pair[0])?;
119 let low = decode_hex_nibble(pair[1])?;
120 Ok((high << 4) | low)
121 })
122 .collect()
123}
124
125const fn decode_hex_nibble(value: u8) -> Result<u8, icydb_schema::SchemaContractError> {
126 match value {
127 b'0'..=b'9' => Ok(value - b'0'),
128 b'a'..=b'f' => Ok(value - b'a' + 10),
129 b'A'..=b'F' => Ok(value - b'A' + 10),
130 _ => Err(icydb_schema::SchemaContractError::InvalidLiteral),
131 }
132}
133
134#[derive(Clone, Debug, Serialize)]
139pub struct Canister {
140 def: Def,
141 memory_namespace: &'static str,
142 memory_profile: CanisterMemoryProfile,
143 memory_min: u8,
144 memory_max: u8,
145 commit_memory_id: u8,
146 startup_memory_id: u8,
147 integrity_progress_memory_id: u8,
148 #[serde(skip)]
149 migration_plan: Option<MigrationPlanConstructor>,
150}
151
152impl Canister {
153 #[must_use]
154 #[expect(
155 clippy::too_many_arguments,
156 reason = "the generated canister node keeps its closed memory identities explicit"
157 )]
158 pub const fn new(
159 def: Def,
160 memory_namespace: &'static str,
161 memory_min: u8,
162 memory_max: u8,
163 commit_memory_id: u8,
164 startup_memory_id: u8,
165 integrity_progress_memory_id: u8,
166 migration_plan: Option<MigrationPlanConstructor>,
167 ) -> Self {
168 Self {
169 def,
170 memory_namespace,
171 memory_profile: CanisterMemoryProfile::General,
172 memory_min,
173 memory_max,
174 commit_memory_id,
175 startup_memory_id,
176 integrity_progress_memory_id,
177 migration_plan,
178 }
179 }
180
181 #[must_use]
182 pub const fn def(&self) -> &Def {
183 &self.def
184 }
185
186 #[must_use]
187 pub const fn memory_namespace(&self) -> &'static str {
188 self.memory_namespace
189 }
190
191 #[must_use]
195 pub const fn with_memory_profile(mut self, profile: CanisterMemoryProfile) -> Self {
196 self.memory_profile = profile;
197 self
198 }
199
200 #[must_use]
202 pub const fn memory_profile(&self) -> CanisterMemoryProfile {
203 self.memory_profile
204 }
205
206 #[must_use]
207 pub const fn memory_min(&self) -> u8 {
208 self.memory_min
209 }
210
211 #[must_use]
212 pub const fn memory_max(&self) -> u8 {
213 self.memory_max
214 }
215
216 #[must_use]
217 pub const fn commit_memory_id(&self) -> u8 {
218 self.commit_memory_id
219 }
220
221 #[must_use]
222 pub const fn startup_memory_id(&self) -> u8 {
223 self.startup_memory_id
224 }
225
226 #[must_use]
227 pub const fn integrity_progress_memory_id(&self) -> u8 {
228 self.integrity_progress_memory_id
229 }
230
231 pub fn migration_plan(
237 &self,
238 ) -> Result<Option<icydb_schema::SchemaMigrationPlan>, icydb_schema::SchemaContractError> {
239 self.migration_plan
240 .map(|constructor| constructor())
241 .transpose()
242 }
243
244 #[must_use]
245 pub fn commit_stable_key(&self) -> String {
246 stable_memory_key(self.memory_namespace(), "commit", "control")
247 }
248
249 #[must_use]
250 pub fn integrity_progress_stable_key(&self) -> String {
251 stable_memory_key(self.memory_namespace(), "integrity", "progress")
252 }
253
254 #[must_use]
255 pub fn startup_stable_key(&self) -> String {
256 stable_memory_key(self.memory_namespace(), "startup", "control")
257 }
258
259 fn validate_declared_memory_contract(&self, errs: &mut ErrorTree) {
260 validate_stable_key_segment(errs, "canister memory_namespace", self.memory_namespace());
261 validate_memory_id_in_range(
262 errs,
263 "commit_memory_id",
264 self.commit_memory_id(),
265 self.memory_min(),
266 self.memory_max(),
267 );
268 validate_app_memory_id(errs, "commit_memory_id", self.commit_memory_id());
269 validate_memory_id_not_reserved(errs, "commit_memory_id", self.commit_memory_id());
270 validate_stable_key(errs, "commit stable key", &self.commit_stable_key());
271 validate_memory_id_in_range(
272 errs,
273 "startup_memory_id",
274 self.startup_memory_id(),
275 self.memory_min(),
276 self.memory_max(),
277 );
278 validate_app_memory_id(errs, "startup_memory_id", self.startup_memory_id());
279 validate_memory_id_not_reserved(errs, "startup_memory_id", self.startup_memory_id());
280 validate_stable_key(errs, "startup stable key", &self.startup_stable_key());
281 validate_memory_id_in_range(
282 errs,
283 "integrity_progress_memory_id",
284 self.integrity_progress_memory_id(),
285 self.memory_min(),
286 self.memory_max(),
287 );
288 validate_app_memory_id(
289 errs,
290 "integrity_progress_memory_id",
291 self.integrity_progress_memory_id(),
292 );
293 validate_memory_id_not_reserved(
294 errs,
295 "integrity_progress_memory_id",
296 self.integrity_progress_memory_id(),
297 );
298 validate_stable_key(
299 errs,
300 "integrity progress stable key",
301 &self.integrity_progress_stable_key(),
302 );
303 }
304
305 fn register_store_allocations(
306 &self,
307 canister_path: &str,
308 seen_ids: &mut BTreeMap<u8, (String, String)>,
309 seen_keys: &mut BTreeMap<String, (u8, String)>,
310 errs: &mut ErrorTree,
311 ) {
312 let schema = schema_read();
313 for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
314 if !matches!(store.storage(), StoreStorage::Journaled(_)) {
315 continue;
316 }
317 for (allocation, role) in [
318 (
319 store.stable_data_allocation(self.memory_namespace()),
320 "data",
321 ),
322 (
323 store.stable_index_allocation(self.memory_namespace()),
324 "index",
325 ),
326 (
327 store.stable_schema_allocation(self.memory_namespace()),
328 "schema",
329 ),
330 ] {
331 assert_unique_memory_allocation(
332 allocation.memory_id(),
333 allocation.stable_key().to_string(),
334 format!("Store `{path}`.{role}_memory"),
335 canister_path,
336 seen_ids,
337 seen_keys,
338 errs,
339 );
340 }
341 if store.is_journaled_storage() {
342 let allocation = store.journal_allocation(self.memory_namespace());
343 assert_unique_memory_allocation(
344 allocation.memory_id(),
345 allocation.stable_key().to_string(),
346 format!("Store `{path}`.journal_memory"),
347 canister_path,
348 seen_ids,
349 seen_keys,
350 errs,
351 );
352 }
353 }
354 }
355}
356
357impl MacroNode for Canister {
358 fn as_any(&self) -> &dyn std::any::Any {
359 self
360 }
361}
362
363impl ValidateNode for Canister {
364 fn validate(&self) -> Result<(), ErrorTree> {
365 let mut errs = ErrorTree::new();
366
367 let canister_path = self.def().path();
368 let mut seen_ids = BTreeMap::<u8, (String, String)>::new();
369 let mut seen_keys = BTreeMap::<String, (u8, String)>::new();
370
371 self.validate_declared_memory_contract(&mut errs);
372
373 assert_unique_memory_allocation(
374 self.commit_memory_id(),
375 self.commit_stable_key(),
376 format!("Canister `{}`.commit_memory", self.def().path()),
377 &canister_path,
378 &mut seen_ids,
379 &mut seen_keys,
380 &mut errs,
381 );
382 assert_unique_memory_allocation(
383 self.startup_memory_id(),
384 self.startup_stable_key(),
385 format!("Canister `{}`.startup_memory", self.def().path()),
386 &canister_path,
387 &mut seen_ids,
388 &mut seen_keys,
389 &mut errs,
390 );
391 assert_unique_memory_allocation(
392 self.integrity_progress_memory_id(),
393 self.integrity_progress_stable_key(),
394 format!("Canister `{}`.integrity_progress_memory", self.def().path()),
395 &canister_path,
396 &mut seen_ids,
397 &mut seen_keys,
398 &mut errs,
399 );
400 self.register_store_allocations(&canister_path, &mut seen_ids, &mut seen_keys, &mut errs);
401
402 errs.result()
403 }
404}
405
406fn assert_unique_memory_allocation(
407 memory_id: u8,
408 stable_key: String,
409 slot: String,
410 canister_path: &str,
411 seen_ids: &mut BTreeMap<u8, (String, String)>,
412 seen_keys: &mut BTreeMap<String, (u8, String)>,
413 errs: &mut ErrorTree,
414) {
415 if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
416 err!(
417 errs,
418 "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
419 memory_id,
420 canister_path,
421 existing_slot,
422 existing_key,
423 slot,
424 stable_key,
425 );
426 } else {
427 seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
428 }
429
430 if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
431 err!(
432 errs,
433 "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
434 stable_key,
435 canister_path,
436 existing_slot,
437 existing_id,
438 slot,
439 memory_id,
440 );
441 } else {
442 seen_keys.insert(stable_key, (memory_id, slot));
443 }
444}
445
446impl VisitableNode for Canister {
447 fn route_key(&self) -> String {
448 self.def().path()
449 }
450}