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