Skip to main content

icydb_schema/node/
canister.rs

1//! Module: node::canister
2//!
3//! Responsibility: canister-level schema node metadata and memory allocation validation.
4//! Does not own: ICP lifecycle management or runtime stable-memory implementation.
5//! Boundary: validates declared memory ranges and stable keys before runtime use.
6
7#[cfg(test)]
8mod tests;
9
10use crate::node::{
11    stable_memory_key, validate_app_memory_id, validate_memory_id_in_range,
12    validate_memory_id_not_reserved, validate_stable_key, validate_stable_key_segment,
13};
14use crate::prelude::*;
15use std::collections::BTreeMap;
16
17///
18/// Canister
19///
20
21#[derive(Clone, Debug, Serialize)]
22pub struct Canister {
23    def: Def,
24    memory_namespace: &'static str,
25    memory_min: u8,
26    memory_max: u8,
27    commit_memory_id: u8,
28}
29
30impl Canister {
31    #[must_use]
32    pub const fn new(
33        def: Def,
34        memory_namespace: &'static str,
35        memory_min: u8,
36        memory_max: u8,
37        commit_memory_id: u8,
38    ) -> Self {
39        Self {
40            def,
41            memory_namespace,
42            memory_min,
43            memory_max,
44            commit_memory_id,
45        }
46    }
47
48    #[must_use]
49    pub const fn def(&self) -> &Def {
50        &self.def
51    }
52
53    #[must_use]
54    pub const fn memory_namespace(&self) -> &'static str {
55        self.memory_namespace
56    }
57
58    #[must_use]
59    pub const fn memory_min(&self) -> u8 {
60        self.memory_min
61    }
62
63    #[must_use]
64    pub const fn memory_max(&self) -> u8 {
65        self.memory_max
66    }
67
68    #[must_use]
69    pub const fn commit_memory_id(&self) -> u8 {
70        self.commit_memory_id
71    }
72
73    #[must_use]
74    pub fn commit_stable_key(&self) -> String {
75        stable_memory_key(self.memory_namespace(), "commit", "control")
76    }
77}
78
79impl MacroNode for Canister {
80    fn as_any(&self) -> &dyn std::any::Any {
81        self
82    }
83}
84
85impl ValidateNode for Canister {
86    fn validate(&self) -> Result<(), ErrorTree> {
87        let mut errs = ErrorTree::new();
88
89        let canister_path = self.def().path();
90        let mut seen_ids = BTreeMap::<u8, (String, String)>::new();
91        let mut seen_keys = BTreeMap::<String, (u8, String)>::new();
92
93        validate_stable_key_segment(
94            &mut errs,
95            "canister memory_namespace",
96            self.memory_namespace(),
97        );
98
99        validate_memory_id_in_range(
100            &mut errs,
101            "commit_memory_id",
102            self.commit_memory_id(),
103            self.memory_min(),
104            self.memory_max(),
105        );
106        validate_app_memory_id(&mut errs, "commit_memory_id", self.commit_memory_id());
107        validate_memory_id_not_reserved(&mut errs, "commit_memory_id", self.commit_memory_id());
108        validate_stable_key(&mut errs, "commit stable key", &self.commit_stable_key());
109
110        assert_unique_memory_allocation(
111            self.commit_memory_id(),
112            self.commit_stable_key(),
113            format!("Canister `{}`.commit_memory", self.def().path()),
114            &canister_path,
115            &mut seen_ids,
116            &mut seen_keys,
117            &mut errs,
118        );
119
120        {
121            let schema = schema_read();
122
123            // Check all Store nodes for this canister
124            for (path, store) in
125                schema.filter_nodes::<Store>(|node| node.canister() == canister_path)
126            {
127                match store.storage() {
128                    StoreStorage::Journaled(_) => {
129                        assert_unique_memory_allocation(
130                            store
131                                .stable_data_allocation(self.memory_namespace())
132                                .memory_id(),
133                            store
134                                .stable_data_allocation(self.memory_namespace())
135                                .stable_key()
136                                .to_string(),
137                            format!("Store `{path}`.data_memory"),
138                            &canister_path,
139                            &mut seen_ids,
140                            &mut seen_keys,
141                            &mut errs,
142                        );
143
144                        assert_unique_memory_allocation(
145                            store
146                                .stable_index_allocation(self.memory_namespace())
147                                .memory_id(),
148                            store
149                                .stable_index_allocation(self.memory_namespace())
150                                .stable_key()
151                                .to_string(),
152                            format!("Store `{path}`.index_memory"),
153                            &canister_path,
154                            &mut seen_ids,
155                            &mut seen_keys,
156                            &mut errs,
157                        );
158
159                        assert_unique_memory_allocation(
160                            store
161                                .stable_schema_allocation(self.memory_namespace())
162                                .memory_id(),
163                            store
164                                .stable_schema_allocation(self.memory_namespace())
165                                .stable_key()
166                                .to_string(),
167                            format!("Store `{path}`.schema_memory"),
168                            &canister_path,
169                            &mut seen_ids,
170                            &mut seen_keys,
171                            &mut errs,
172                        );
173
174                        if store.is_journaled_storage() {
175                            assert_unique_memory_allocation(
176                                store
177                                    .journal_allocation(self.memory_namespace())
178                                    .memory_id(),
179                                store
180                                    .journal_allocation(self.memory_namespace())
181                                    .stable_key()
182                                    .to_string(),
183                                format!("Store `{path}`.journal_memory"),
184                                &canister_path,
185                                &mut seen_ids,
186                                &mut seen_keys,
187                                &mut errs,
188                            );
189                        }
190                    }
191                    StoreStorage::Heap(_) => {}
192                }
193            }
194        }
195
196        errs.result()
197    }
198}
199
200fn assert_unique_memory_allocation(
201    memory_id: u8,
202    stable_key: String,
203    slot: String,
204    canister_path: &str,
205    seen_ids: &mut BTreeMap<u8, (String, String)>,
206    seen_keys: &mut BTreeMap<String, (u8, String)>,
207    errs: &mut ErrorTree,
208) {
209    if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
210        err!(
211            errs,
212            "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
213            memory_id,
214            canister_path,
215            existing_slot,
216            existing_key,
217            slot,
218            stable_key,
219        );
220    } else {
221        seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
222    }
223
224    if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
225        err!(
226            errs,
227            "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
228            stable_key,
229            canister_path,
230            existing_slot,
231            existing_id,
232            slot,
233            memory_id,
234        );
235    } else {
236        seen_keys.insert(stable_key, (memory_id, slot));
237    }
238}
239
240impl VisitableNode for Canister {
241    fn route_key(&self) -> String {
242        self.def().path()
243    }
244}