1use crate::node::{
2 stable_memory_key, validate_app_memory_id, validate_memory_id_in_range,
3 validate_memory_id_not_reserved, validate_stable_key, validate_stable_key_segment,
4};
5use crate::prelude::*;
6use std::collections::BTreeMap;
7
8#[derive(CandidType, Clone, Debug, Serialize)]
13pub struct Canister {
14 def: Def,
15 memory_namespace: &'static str,
16 memory_min: u8,
17 memory_max: u8,
18 commit_memory_id: u8,
19}
20
21impl Canister {
22 #[must_use]
23 pub const fn new(
24 def: Def,
25 memory_namespace: &'static str,
26 memory_min: u8,
27 memory_max: u8,
28 commit_memory_id: u8,
29 ) -> Self {
30 Self {
31 def,
32 memory_namespace,
33 memory_min,
34 memory_max,
35 commit_memory_id,
36 }
37 }
38
39 #[must_use]
40 pub const fn def(&self) -> &Def {
41 &self.def
42 }
43
44 #[must_use]
45 pub const fn memory_namespace(&self) -> &'static str {
46 self.memory_namespace
47 }
48
49 #[must_use]
50 pub const fn memory_min(&self) -> u8 {
51 self.memory_min
52 }
53
54 #[must_use]
55 pub const fn memory_max(&self) -> u8 {
56 self.memory_max
57 }
58
59 #[must_use]
60 pub const fn commit_memory_id(&self) -> u8 {
61 self.commit_memory_id
62 }
63
64 #[must_use]
65 pub fn commit_stable_key(&self) -> String {
66 stable_memory_key(self.memory_namespace(), "commit", "control")
67 }
68}
69
70impl MacroNode for Canister {
71 fn as_any(&self) -> &dyn std::any::Any {
72 self
73 }
74}
75
76impl ValidateNode for Canister {
77 fn validate(&self) -> Result<(), ErrorTree> {
78 let mut errs = ErrorTree::new();
79 let schema = schema_read();
80
81 let canister_path = self.def().path();
82 let mut seen_ids = BTreeMap::<u8, (String, String)>::new();
83 let mut seen_keys = BTreeMap::<String, (u8, String)>::new();
84
85 validate_stable_key_segment(
86 &mut errs,
87 "canister memory_namespace",
88 self.memory_namespace(),
89 );
90
91 validate_memory_id_in_range(
92 &mut errs,
93 "commit_memory_id",
94 self.commit_memory_id(),
95 self.memory_min(),
96 self.memory_max(),
97 );
98 validate_app_memory_id(&mut errs, "commit_memory_id", self.commit_memory_id());
99 validate_memory_id_not_reserved(&mut errs, "commit_memory_id", self.commit_memory_id());
100 validate_stable_key(&mut errs, "commit stable key", &self.commit_stable_key());
101
102 assert_unique_memory_allocation(
103 self.commit_memory_id(),
104 self.commit_stable_key(),
105 format!("Canister `{}`.commit_memory", self.def().path()),
106 &canister_path,
107 &mut seen_ids,
108 &mut seen_keys,
109 &mut errs,
110 );
111
112 for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
114 match store.storage() {
115 StoreStorage::Stable(_) | StoreStorage::Journaled(_) => {
116 assert_unique_memory_allocation(
117 store
118 .stable_data_allocation(self.memory_namespace())
119 .memory_id(),
120 store
121 .stable_data_allocation(self.memory_namespace())
122 .stable_key()
123 .to_string(),
124 format!("Store `{path}`.data_memory"),
125 &canister_path,
126 &mut seen_ids,
127 &mut seen_keys,
128 &mut errs,
129 );
130
131 assert_unique_memory_allocation(
132 store
133 .stable_index_allocation(self.memory_namespace())
134 .memory_id(),
135 store
136 .stable_index_allocation(self.memory_namespace())
137 .stable_key()
138 .to_string(),
139 format!("Store `{path}`.index_memory"),
140 &canister_path,
141 &mut seen_ids,
142 &mut seen_keys,
143 &mut errs,
144 );
145
146 assert_unique_memory_allocation(
147 store
148 .stable_schema_allocation(self.memory_namespace())
149 .memory_id(),
150 store
151 .stable_schema_allocation(self.memory_namespace())
152 .stable_key()
153 .to_string(),
154 format!("Store `{path}`.schema_memory"),
155 &canister_path,
156 &mut seen_ids,
157 &mut seen_keys,
158 &mut errs,
159 );
160
161 if store.is_journaled_storage() {
162 assert_unique_memory_allocation(
163 store
164 .journal_allocation(self.memory_namespace())
165 .memory_id(),
166 store
167 .journal_allocation(self.memory_namespace())
168 .stable_key()
169 .to_string(),
170 format!("Store `{path}`.journal_memory"),
171 &canister_path,
172 &mut seen_ids,
173 &mut seen_keys,
174 &mut errs,
175 );
176 }
177 }
178 StoreStorage::Heap(_) => {}
179 }
180 }
181
182 errs.result()
183 }
184}
185
186fn assert_unique_memory_allocation(
187 memory_id: u8,
188 stable_key: String,
189 slot: String,
190 canister_path: &str,
191 seen_ids: &mut BTreeMap<u8, (String, String)>,
192 seen_keys: &mut BTreeMap<String, (u8, String)>,
193 errs: &mut ErrorTree,
194) {
195 if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
196 err!(
197 errs,
198 "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
199 memory_id,
200 canister_path,
201 existing_slot,
202 existing_key,
203 slot,
204 stable_key,
205 );
206 } else {
207 seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
208 }
209
210 if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
211 err!(
212 errs,
213 "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
214 stable_key,
215 canister_path,
216 existing_slot,
217 existing_id,
218 slot,
219 memory_id,
220 );
221 } else {
222 seen_keys.insert(stable_key, (memory_id, slot));
223 }
224}
225
226impl VisitableNode for Canister {
227 fn route_key(&self) -> String {
228 self.def().path()
229 }
230}
231
232#[cfg(test)]
237mod tests {
238 use crate::build::schema_write;
239
240 use super::*;
241
242 fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
243 let canister = Canister::new(Def::new(path_module, ident), "test_db", 100, 254, 254);
244 schema_write().insert_node(SchemaNode::Canister(canister.clone()));
245
246 canister
247 }
248
249 fn insert_store(
250 path_module: &'static str,
251 ident: &'static str,
252 store_name: &'static str,
253 canister_path: &'static str,
254 data_memory_id: u8,
255 index_memory_id: u8,
256 schema_memory_id: u8,
257 ) {
258 schema_write().insert_node(SchemaNode::Store(Store::new_stable(
259 Def::new(path_module, ident),
260 ident,
261 store_name,
262 canister_path,
263 StoreStableMemoryConfig::new(data_memory_id, index_memory_id, schema_memory_id),
264 )));
265 }
266
267 #[test]
268 fn validate_rejects_memory_id_collision_between_stores() {
269 let canister = insert_canister("schema_store_collision", "Canister");
270 let canister_path = "schema_store_collision::Canister";
271
272 insert_store(
273 "schema_store_collision",
274 "StoreA",
275 "store_a",
276 canister_path,
277 110,
278 111,
279 112,
280 );
281 insert_store(
282 "schema_store_collision",
283 "StoreB",
284 "store_b",
285 canister_path,
286 113,
287 110,
288 114,
289 ); let err = canister
292 .validate()
293 .expect_err("memory-id collision must fail");
294
295 let rendered = err.to_string();
296 assert!(
297 rendered.contains("duplicate memory_id `110`"),
298 "expected duplicate memory-id error, got: {rendered}"
299 );
300 }
301
302 #[test]
303 fn validate_accepts_unique_memory_ids() {
304 let canister = insert_canister("schema_store_unique", "Canister");
305 let canister_path = "schema_store_unique::Canister";
306
307 insert_store(
308 "schema_store_unique",
309 "StoreA",
310 "store_a",
311 canister_path,
312 130,
313 131,
314 132,
315 );
316 insert_store(
317 "schema_store_unique",
318 "StoreB",
319 "store_b",
320 canister_path,
321 133,
322 134,
323 135,
324 );
325
326 canister.validate().expect("unique memory IDs should pass");
327 }
328
329 #[test]
330 fn validate_rejects_reserved_commit_memory_id() {
331 let canister = Canister::new(
332 Def::new("schema_reserved_commit", "Canister"),
333 "test_db",
334 100,
335 254,
336 255,
337 );
338 schema_write().insert_node(SchemaNode::Canister(canister.clone()));
339
340 let err = canister
341 .validate()
342 .expect_err("reserved commit memory id must fail");
343
344 let rendered = err.to_string();
345 assert!(
346 rendered.contains("reserved for stable-structures internals"),
347 "expected reserved-id error, got: {rendered}"
348 );
349 }
350
351 #[test]
352 fn store_allocation_identity_is_independent_of_schema_order() {
353 let first = Store::new_stable(
354 Def::new("schema_allocation_order", "Users"),
355 "USERS",
356 "users",
357 "schema_allocation_order::Canister",
358 StoreStableMemoryConfig::new(110, 111, 112),
359 );
360 let reordered = Store::new_stable(
361 Def::new("schema_allocation_order", "Users"),
362 "USERS",
363 "users",
364 "schema_allocation_order::Canister",
365 StoreStableMemoryConfig::new(110, 111, 112),
366 );
367
368 assert!(
369 first
370 .stable_data_allocation("test_db")
371 .same_identity_as(&reordered.stable_data_allocation("test_db"))
372 );
373 assert!(
374 first
375 .stable_index_allocation("test_db")
376 .same_identity_as(&reordered.stable_index_allocation("test_db"))
377 );
378 assert!(
379 first
380 .stable_schema_allocation("test_db")
381 .same_identity_as(&reordered.stable_schema_allocation("test_db"))
382 );
383 }
384
385 #[test]
386 fn adding_store_does_not_change_existing_store_allocation() {
387 let existing = Store::new_stable(
388 Def::new("schema_allocation_add", "Users"),
389 "USERS",
390 "users",
391 "schema_allocation_add::Canister",
392 StoreStableMemoryConfig::new(110, 111, 112),
393 );
394 let _new_store = Store::new_stable(
395 Def::new("schema_allocation_add", "AuditEvents"),
396 "AUDIT_EVENTS",
397 "audit_events",
398 "schema_allocation_add::Canister",
399 StoreStableMemoryConfig::new(120, 121, 122),
400 );
401
402 assert_eq!(existing.stable_data_allocation("test_db").memory_id(), 110);
403 assert_eq!(
404 existing.stable_data_allocation("test_db").stable_key(),
405 "icydb.test_db.users.data.v1"
406 );
407 }
408
409 #[test]
410 fn validate_rejects_same_stable_key_with_different_memory_id() {
411 let canister = insert_canister("schema_store_key_collision", "Canister");
412 let canister_path = "schema_store_key_collision::Canister";
413
414 insert_store(
415 "schema_store_key_collision",
416 "StoreA",
417 "users",
418 canister_path,
419 110,
420 111,
421 112,
422 );
423 insert_store(
424 "schema_store_key_collision",
425 "StoreB",
426 "users",
427 canister_path,
428 120,
429 121,
430 122,
431 );
432
433 let err = canister
434 .validate()
435 .expect_err("stable-key collision must fail");
436
437 let rendered = err.to_string();
438 assert!(
439 rendered.contains("duplicate stable_key `icydb.test_db.users.data.v1`"),
440 "expected duplicate stable-key error, got: {rendered}"
441 );
442 }
443
444 #[test]
445 fn stable_memory_identity_ignores_schema_metadata() {
446 let left = StableMemoryAllocation::with_schema_metadata(
447 110,
448 "icydb.test_db.users.data.v1".to_string(),
449 StableMemoryAllocationMetadata::from_accepted_schema_contract(1, "aaa".to_string()),
450 );
451 let right = StableMemoryAllocation::with_schema_metadata(
452 110,
453 "icydb.test_db.users.data.v1".to_string(),
454 StableMemoryAllocationMetadata::from_accepted_schema_contract(2, "bbb".to_string()),
455 );
456
457 assert!(left.same_identity_as(&right));
458 }
459
460 #[test]
461 fn validate_rejects_app_memory_id_below_canic_reserved_range() {
462 let canister = Canister::new(
463 Def::new("schema_reserved_app_range", "Canister"),
464 "test_db",
465 99,
466 110,
467 99,
468 );
469
470 let err = canister
471 .validate()
472 .expect_err("app memory id below 100 must fail");
473
474 let rendered = err.to_string();
475 assert!(
476 rendered.contains("outside of application memory range 100-254"),
477 "expected app memory range error, got: {rendered}"
478 );
479 }
480
481 #[test]
482 fn stable_keys_reject_canic_prefix() {
483 assert!(!stable_key_is_canonical("canic.test.users.data.v1"));
484 }
485}