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