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