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