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(_) => {
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 StoreStorage::Heap(_) => {}
162 }
163 }
164
165 errs.result()
166 }
167}
168
169fn assert_unique_memory_allocation(
170 memory_id: u8,
171 stable_key: String,
172 slot: String,
173 canister_path: &str,
174 seen_ids: &mut BTreeMap<u8, (String, String)>,
175 seen_keys: &mut BTreeMap<String, (u8, String)>,
176 errs: &mut ErrorTree,
177) {
178 if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
179 err!(
180 errs,
181 "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
182 memory_id,
183 canister_path,
184 existing_slot,
185 existing_key,
186 slot,
187 stable_key,
188 );
189 } else {
190 seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
191 }
192
193 if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
194 err!(
195 errs,
196 "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
197 stable_key,
198 canister_path,
199 existing_slot,
200 existing_id,
201 slot,
202 memory_id,
203 );
204 } else {
205 seen_keys.insert(stable_key, (memory_id, slot));
206 }
207}
208
209impl VisitableNode for Canister {
210 fn route_key(&self) -> String {
211 self.def().path()
212 }
213}
214
215#[cfg(test)]
220mod tests {
221 use crate::build::schema_write;
222
223 use super::*;
224
225 fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
226 let canister = Canister::new(Def::new(path_module, ident), "test_db", 100, 254, 254);
227 schema_write().insert_node(SchemaNode::Canister(canister.clone()));
228
229 canister
230 }
231
232 fn insert_store(
233 path_module: &'static str,
234 ident: &'static str,
235 store_name: &'static str,
236 canister_path: &'static str,
237 data_memory_id: u8,
238 index_memory_id: u8,
239 schema_memory_id: u8,
240 ) {
241 schema_write().insert_node(SchemaNode::Store(Store::new_stable(
242 Def::new(path_module, ident),
243 ident,
244 store_name,
245 canister_path,
246 StoreStableMemoryConfig::new(data_memory_id, index_memory_id, schema_memory_id),
247 )));
248 }
249
250 #[test]
251 fn validate_rejects_memory_id_collision_between_stores() {
252 let canister = insert_canister("schema_store_collision", "Canister");
253 let canister_path = "schema_store_collision::Canister";
254
255 insert_store(
256 "schema_store_collision",
257 "StoreA",
258 "store_a",
259 canister_path,
260 110,
261 111,
262 112,
263 );
264 insert_store(
265 "schema_store_collision",
266 "StoreB",
267 "store_b",
268 canister_path,
269 113,
270 110,
271 114,
272 ); let err = canister
275 .validate()
276 .expect_err("memory-id collision must fail");
277
278 let rendered = err.to_string();
279 assert!(
280 rendered.contains("duplicate memory_id `110`"),
281 "expected duplicate memory-id error, got: {rendered}"
282 );
283 }
284
285 #[test]
286 fn validate_accepts_unique_memory_ids() {
287 let canister = insert_canister("schema_store_unique", "Canister");
288 let canister_path = "schema_store_unique::Canister";
289
290 insert_store(
291 "schema_store_unique",
292 "StoreA",
293 "store_a",
294 canister_path,
295 130,
296 131,
297 132,
298 );
299 insert_store(
300 "schema_store_unique",
301 "StoreB",
302 "store_b",
303 canister_path,
304 133,
305 134,
306 135,
307 );
308
309 canister.validate().expect("unique memory IDs should pass");
310 }
311
312 #[test]
313 fn validate_rejects_reserved_commit_memory_id() {
314 let canister = Canister::new(
315 Def::new("schema_reserved_commit", "Canister"),
316 "test_db",
317 100,
318 254,
319 255,
320 );
321 schema_write().insert_node(SchemaNode::Canister(canister.clone()));
322
323 let err = canister
324 .validate()
325 .expect_err("reserved commit memory id must fail");
326
327 let rendered = err.to_string();
328 assert!(
329 rendered.contains("reserved for stable-structures internals"),
330 "expected reserved-id error, got: {rendered}"
331 );
332 }
333
334 #[test]
335 fn store_allocation_identity_is_independent_of_schema_order() {
336 let first = Store::new_stable(
337 Def::new("schema_allocation_order", "Users"),
338 "USERS",
339 "users",
340 "schema_allocation_order::Canister",
341 StoreStableMemoryConfig::new(110, 111, 112),
342 );
343 let reordered = Store::new_stable(
344 Def::new("schema_allocation_order", "Users"),
345 "USERS",
346 "users",
347 "schema_allocation_order::Canister",
348 StoreStableMemoryConfig::new(110, 111, 112),
349 );
350
351 assert!(
352 first
353 .stable_data_allocation("test_db")
354 .same_identity_as(&reordered.stable_data_allocation("test_db"))
355 );
356 assert!(
357 first
358 .stable_index_allocation("test_db")
359 .same_identity_as(&reordered.stable_index_allocation("test_db"))
360 );
361 assert!(
362 first
363 .stable_schema_allocation("test_db")
364 .same_identity_as(&reordered.stable_schema_allocation("test_db"))
365 );
366 }
367
368 #[test]
369 fn adding_store_does_not_change_existing_store_allocation() {
370 let existing = Store::new_stable(
371 Def::new("schema_allocation_add", "Users"),
372 "USERS",
373 "users",
374 "schema_allocation_add::Canister",
375 StoreStableMemoryConfig::new(110, 111, 112),
376 );
377 let _new_store = Store::new_stable(
378 Def::new("schema_allocation_add", "AuditEvents"),
379 "AUDIT_EVENTS",
380 "audit_events",
381 "schema_allocation_add::Canister",
382 StoreStableMemoryConfig::new(120, 121, 122),
383 );
384
385 assert_eq!(existing.stable_data_allocation("test_db").memory_id(), 110);
386 assert_eq!(
387 existing.stable_data_allocation("test_db").stable_key(),
388 "icydb.test_db.users.data.v1"
389 );
390 }
391
392 #[test]
393 fn validate_rejects_same_stable_key_with_different_memory_id() {
394 let canister = insert_canister("schema_store_key_collision", "Canister");
395 let canister_path = "schema_store_key_collision::Canister";
396
397 insert_store(
398 "schema_store_key_collision",
399 "StoreA",
400 "users",
401 canister_path,
402 110,
403 111,
404 112,
405 );
406 insert_store(
407 "schema_store_key_collision",
408 "StoreB",
409 "users",
410 canister_path,
411 120,
412 121,
413 122,
414 );
415
416 let err = canister
417 .validate()
418 .expect_err("stable-key collision must fail");
419
420 let rendered = err.to_string();
421 assert!(
422 rendered.contains("duplicate stable_key `icydb.test_db.users.data.v1`"),
423 "expected duplicate stable-key error, got: {rendered}"
424 );
425 }
426
427 #[test]
428 fn stable_memory_identity_ignores_schema_metadata() {
429 let left = StableMemoryAllocation::with_schema_metadata(
430 110,
431 "icydb.test_db.users.data.v1".to_string(),
432 StableMemoryAllocationMetadata::from_accepted_schema_contract(1, "aaa".to_string()),
433 );
434 let right = StableMemoryAllocation::with_schema_metadata(
435 110,
436 "icydb.test_db.users.data.v1".to_string(),
437 StableMemoryAllocationMetadata::from_accepted_schema_contract(2, "bbb".to_string()),
438 );
439
440 assert!(left.same_identity_as(&right));
441 }
442
443 #[test]
444 fn validate_rejects_app_memory_id_below_canic_reserved_range() {
445 let canister = Canister::new(
446 Def::new("schema_reserved_app_range", "Canister"),
447 "test_db",
448 99,
449 110,
450 99,
451 );
452
453 let err = canister
454 .validate()
455 .expect_err("app memory id below 100 must fail");
456
457 let rendered = err.to_string();
458 assert!(
459 rendered.contains("outside of application memory range 100-254"),
460 "expected app memory range error, got: {rendered}"
461 );
462 }
463
464 #[test]
465 fn stable_keys_reject_canic_prefix() {
466 assert!(!stable_key_is_canonical("canic.test.users.data.v1"));
467 }
468}