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    integrity_progress_memory_id: u8,
29}
30
31impl Canister {
32    #[must_use]
33    pub const fn new(
34        def: Def,
35        memory_namespace: &'static str,
36        memory_min: u8,
37        memory_max: u8,
38        commit_memory_id: u8,
39        integrity_progress_memory_id: u8,
40    ) -> Self {
41        Self {
42            def,
43            memory_namespace,
44            memory_min,
45            memory_max,
46            commit_memory_id,
47            integrity_progress_memory_id,
48        }
49    }
50
51    #[must_use]
52    pub const fn def(&self) -> &Def {
53        &self.def
54    }
55
56    #[must_use]
57    pub const fn memory_namespace(&self) -> &'static str {
58        self.memory_namespace
59    }
60
61    #[must_use]
62    pub const fn memory_min(&self) -> u8 {
63        self.memory_min
64    }
65
66    #[must_use]
67    pub const fn memory_max(&self) -> u8 {
68        self.memory_max
69    }
70
71    #[must_use]
72    pub const fn commit_memory_id(&self) -> u8 {
73        self.commit_memory_id
74    }
75
76    #[must_use]
77    pub const fn integrity_progress_memory_id(&self) -> u8 {
78        self.integrity_progress_memory_id
79    }
80
81    #[must_use]
82    pub fn commit_stable_key(&self) -> String {
83        stable_memory_key(self.memory_namespace(), "commit", "control")
84    }
85
86    #[must_use]
87    pub fn integrity_progress_stable_key(&self) -> String {
88        stable_memory_key(self.memory_namespace(), "integrity", "progress")
89    }
90
91    fn validate_declared_memory_contract(&self, errs: &mut ErrorTree) {
92        validate_stable_key_segment(errs, "canister memory_namespace", self.memory_namespace());
93        validate_memory_id_in_range(
94            errs,
95            "commit_memory_id",
96            self.commit_memory_id(),
97            self.memory_min(),
98            self.memory_max(),
99        );
100        validate_app_memory_id(errs, "commit_memory_id", self.commit_memory_id());
101        validate_memory_id_not_reserved(errs, "commit_memory_id", self.commit_memory_id());
102        validate_stable_key(errs, "commit stable key", &self.commit_stable_key());
103        validate_memory_id_in_range(
104            errs,
105            "integrity_progress_memory_id",
106            self.integrity_progress_memory_id(),
107            self.memory_min(),
108            self.memory_max(),
109        );
110        validate_app_memory_id(
111            errs,
112            "integrity_progress_memory_id",
113            self.integrity_progress_memory_id(),
114        );
115        validate_memory_id_not_reserved(
116            errs,
117            "integrity_progress_memory_id",
118            self.integrity_progress_memory_id(),
119        );
120        validate_stable_key(
121            errs,
122            "integrity progress stable key",
123            &self.integrity_progress_stable_key(),
124        );
125    }
126
127    fn register_store_allocations(
128        &self,
129        canister_path: &str,
130        seen_ids: &mut BTreeMap<u8, (String, String)>,
131        seen_keys: &mut BTreeMap<String, (u8, String)>,
132        errs: &mut ErrorTree,
133    ) {
134        let schema = schema_read();
135        for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
136            if !matches!(store.storage(), StoreStorage::Journaled(_)) {
137                continue;
138            }
139            for (allocation, role) in [
140                (
141                    store.stable_data_allocation(self.memory_namespace()),
142                    "data",
143                ),
144                (
145                    store.stable_index_allocation(self.memory_namespace()),
146                    "index",
147                ),
148                (
149                    store.stable_schema_allocation(self.memory_namespace()),
150                    "schema",
151                ),
152            ] {
153                assert_unique_memory_allocation(
154                    allocation.memory_id(),
155                    allocation.stable_key().to_string(),
156                    format!("Store `{path}`.{role}_memory"),
157                    canister_path,
158                    seen_ids,
159                    seen_keys,
160                    errs,
161                );
162            }
163            if store.is_journaled_storage() {
164                let allocation = store.journal_allocation(self.memory_namespace());
165                assert_unique_memory_allocation(
166                    allocation.memory_id(),
167                    allocation.stable_key().to_string(),
168                    format!("Store `{path}`.journal_memory"),
169                    canister_path,
170                    seen_ids,
171                    seen_keys,
172                    errs,
173                );
174            }
175        }
176    }
177}
178
179impl MacroNode for Canister {
180    fn as_any(&self) -> &dyn std::any::Any {
181        self
182    }
183}
184
185impl ValidateNode for Canister {
186    fn validate(&self) -> Result<(), ErrorTree> {
187        let mut errs = ErrorTree::new();
188
189        let canister_path = self.def().path();
190        let mut seen_ids = BTreeMap::<u8, (String, String)>::new();
191        let mut seen_keys = BTreeMap::<String, (u8, String)>::new();
192
193        self.validate_declared_memory_contract(&mut errs);
194
195        assert_unique_memory_allocation(
196            self.commit_memory_id(),
197            self.commit_stable_key(),
198            format!("Canister `{}`.commit_memory", self.def().path()),
199            &canister_path,
200            &mut seen_ids,
201            &mut seen_keys,
202            &mut errs,
203        );
204        assert_unique_memory_allocation(
205            self.integrity_progress_memory_id(),
206            self.integrity_progress_stable_key(),
207            format!("Canister `{}`.integrity_progress_memory", self.def().path()),
208            &canister_path,
209            &mut seen_ids,
210            &mut seen_keys,
211            &mut errs,
212        );
213        self.register_store_allocations(&canister_path, &mut seen_ids, &mut seen_keys, &mut errs);
214
215        errs.result()
216    }
217}
218
219fn assert_unique_memory_allocation(
220    memory_id: u8,
221    stable_key: String,
222    slot: String,
223    canister_path: &str,
224    seen_ids: &mut BTreeMap<u8, (String, String)>,
225    seen_keys: &mut BTreeMap<String, (u8, String)>,
226    errs: &mut ErrorTree,
227) {
228    if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
229        err!(
230            errs,
231            "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
232            memory_id,
233            canister_path,
234            existing_slot,
235            existing_key,
236            slot,
237            stable_key,
238        );
239    } else {
240        seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
241    }
242
243    if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
244        err!(
245            errs,
246            "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
247            stable_key,
248            canister_path,
249            existing_slot,
250            existing_id,
251            slot,
252            memory_id,
253        );
254    } else {
255        seen_keys.insert(stable_key, (memory_id, slot));
256    }
257}
258
259impl VisitableNode for Canister {
260    fn route_key(&self) -> String {
261        self.def().path()
262    }
263}