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