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        let schema = schema_read();
89
90        let canister_path = self.def().path();
91        let mut seen_ids = BTreeMap::<u8, (String, String)>::new();
92        let mut seen_keys = BTreeMap::<String, (u8, String)>::new();
93
94        validate_stable_key_segment(
95            &mut errs,
96            "canister memory_namespace",
97            self.memory_namespace(),
98        );
99
100        validate_memory_id_in_range(
101            &mut errs,
102            "commit_memory_id",
103            self.commit_memory_id(),
104            self.memory_min(),
105            self.memory_max(),
106        );
107        validate_app_memory_id(&mut errs, "commit_memory_id", self.commit_memory_id());
108        validate_memory_id_not_reserved(&mut errs, "commit_memory_id", self.commit_memory_id());
109        validate_stable_key(&mut errs, "commit stable key", &self.commit_stable_key());
110
111        assert_unique_memory_allocation(
112            self.commit_memory_id(),
113            self.commit_stable_key(),
114            format!("Canister `{}`.commit_memory", self.def().path()),
115            &canister_path,
116            &mut seen_ids,
117            &mut seen_keys,
118            &mut errs,
119        );
120
121        // Check all Store nodes for this canister
122        for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
123            match store.storage() {
124                StoreStorage::Journaled(_) => {
125                    assert_unique_memory_allocation(
126                        store
127                            .stable_data_allocation(self.memory_namespace())
128                            .memory_id(),
129                        store
130                            .stable_data_allocation(self.memory_namespace())
131                            .stable_key()
132                            .to_string(),
133                        format!("Store `{path}`.data_memory"),
134                        &canister_path,
135                        &mut seen_ids,
136                        &mut seen_keys,
137                        &mut errs,
138                    );
139
140                    assert_unique_memory_allocation(
141                        store
142                            .stable_index_allocation(self.memory_namespace())
143                            .memory_id(),
144                        store
145                            .stable_index_allocation(self.memory_namespace())
146                            .stable_key()
147                            .to_string(),
148                        format!("Store `{path}`.index_memory"),
149                        &canister_path,
150                        &mut seen_ids,
151                        &mut seen_keys,
152                        &mut errs,
153                    );
154
155                    assert_unique_memory_allocation(
156                        store
157                            .stable_schema_allocation(self.memory_namespace())
158                            .memory_id(),
159                        store
160                            .stable_schema_allocation(self.memory_namespace())
161                            .stable_key()
162                            .to_string(),
163                        format!("Store `{path}`.schema_memory"),
164                        &canister_path,
165                        &mut seen_ids,
166                        &mut seen_keys,
167                        &mut errs,
168                    );
169
170                    if store.is_journaled_storage() {
171                        assert_unique_memory_allocation(
172                            store
173                                .journal_allocation(self.memory_namespace())
174                                .memory_id(),
175                            store
176                                .journal_allocation(self.memory_namespace())
177                                .stable_key()
178                                .to_string(),
179                            format!("Store `{path}`.journal_memory"),
180                            &canister_path,
181                            &mut seen_ids,
182                            &mut seen_keys,
183                            &mut errs,
184                        );
185                    }
186                }
187                StoreStorage::Heap(_) => {}
188            }
189        }
190
191        errs.result()
192    }
193}
194
195fn assert_unique_memory_allocation(
196    memory_id: u8,
197    stable_key: String,
198    slot: String,
199    canister_path: &str,
200    seen_ids: &mut BTreeMap<u8, (String, String)>,
201    seen_keys: &mut BTreeMap<String, (u8, String)>,
202    errs: &mut ErrorTree,
203) {
204    if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
205        err!(
206            errs,
207            "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
208            memory_id,
209            canister_path,
210            existing_slot,
211            existing_key,
212            slot,
213            stable_key,
214        );
215    } else {
216        seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
217    }
218
219    if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
220        err!(
221            errs,
222            "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
223            stable_key,
224            canister_path,
225            existing_slot,
226            existing_id,
227            slot,
228            memory_id,
229        );
230    } else {
231        seen_keys.insert(stable_key, (memory_id, slot));
232    }
233}
234
235impl VisitableNode for Canister {
236    fn route_key(&self) -> String {
237        self.def().path()
238    }
239}