Skip to main content

icydb_schema/node/
canister.rs

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///
9/// Canister
10///
11
12#[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        // Check all Store nodes for this canister
113        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//
233// TESTS
234//
235
236#[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        ); // collision
290
291        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}