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