1use crate::node::{validate_memory_id_in_range, validate_memory_id_not_reserved};
2use crate::prelude::*;
3use std::collections::BTreeMap;
4
5#[derive(CandidType, Clone, Debug, Serialize)]
10pub struct Canister {
11 def: Def,
12 memory_min: u8,
13 memory_max: u8,
14 pub commit_memory_id: u8,
15}
16
17impl Canister {
18 #[must_use]
19 pub const fn new(def: Def, memory_min: u8, memory_max: u8, commit_memory_id: u8) -> Self {
20 Self {
21 def,
22 memory_min,
23 memory_max,
24 commit_memory_id,
25 }
26 }
27
28 #[must_use]
29 pub const fn def(&self) -> &Def {
30 &self.def
31 }
32
33 #[must_use]
34 pub const fn memory_min(&self) -> u8 {
35 self.memory_min
36 }
37
38 #[must_use]
39 pub const fn memory_max(&self) -> u8 {
40 self.memory_max
41 }
42
43 #[must_use]
44 pub const fn commit_memory_id(&self) -> u8 {
45 self.commit_memory_id
46 }
47}
48
49impl MacroNode for Canister {
50 fn as_any(&self) -> &dyn std::any::Any {
51 self
52 }
53}
54
55impl ValidateNode for Canister {
56 fn validate(&self) -> Result<(), ErrorTree> {
57 let mut errs = ErrorTree::new();
58 let schema = schema_read();
59
60 let canister_path = self.def().path();
61 let mut seen_ids = BTreeMap::<u8, String>::new();
62
63 validate_memory_id_in_range(
64 &mut errs,
65 "commit_memory_id",
66 self.commit_memory_id(),
67 self.memory_min(),
68 self.memory_max(),
69 );
70 validate_memory_id_not_reserved(&mut errs, "commit_memory_id", self.commit_memory_id());
71
72 assert_unique_memory_id(
73 self.commit_memory_id(),
74 format!("Canister `{}`.commit_memory_id", self.def().path()),
75 &canister_path,
76 &mut seen_ids,
77 &mut errs,
78 );
79
80 for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
82 assert_unique_memory_id(
83 store.data_memory_id(),
84 format!("Store `{path}`.data_memory_id"),
85 &canister_path,
86 &mut seen_ids,
87 &mut errs,
88 );
89
90 assert_unique_memory_id(
91 store.index_memory_id(),
92 format!("Store `{path}`.index_memory_id"),
93 &canister_path,
94 &mut seen_ids,
95 &mut errs,
96 );
97
98 assert_unique_memory_id(
99 store.schema_memory_id(),
100 format!("Store `{path}`.schema_memory_id"),
101 &canister_path,
102 &mut seen_ids,
103 &mut errs,
104 );
105 }
106
107 errs.result()
108 }
109}
110
111fn assert_unique_memory_id(
112 memory_id: u8,
113 slot: String,
114 canister_path: &str,
115 seen_ids: &mut BTreeMap<u8, String>,
116 errs: &mut ErrorTree,
117) {
118 if let Some(existing) = seen_ids.get(&memory_id) {
119 err!(
120 errs,
121 "duplicate memory_id `{}` used in canister `{}`: {} conflicts with {}",
122 memory_id,
123 canister_path,
124 existing,
125 slot
126 );
127 } else {
128 seen_ids.insert(memory_id, slot);
129 }
130}
131
132impl VisitableNode for Canister {
133 fn route_key(&self) -> String {
134 self.def().path()
135 }
136}
137
138#[cfg(test)]
143mod tests {
144 use crate::build::schema_write;
145
146 use super::*;
147
148 fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
149 let canister = Canister::new(Def::new(path_module, ident), 0, 255, 254);
150 schema_write().insert_node(SchemaNode::Canister(canister.clone()));
151
152 canister
153 }
154
155 fn insert_store(
156 path_module: &'static str,
157 ident: &'static str,
158 canister_path: &'static str,
159 data_memory_id: u8,
160 index_memory_id: u8,
161 schema_memory_id: u8,
162 ) {
163 schema_write().insert_node(SchemaNode::Store(Store::new(
164 Def::new(path_module, ident),
165 ident,
166 canister_path,
167 data_memory_id,
168 index_memory_id,
169 schema_memory_id,
170 )));
171 }
172
173 #[test]
174 fn validate_rejects_memory_id_collision_between_stores() {
175 let canister = insert_canister("schema_store_collision", "Canister");
176 let canister_path = "schema_store_collision::Canister";
177
178 insert_store(
179 "schema_store_collision",
180 "StoreA",
181 canister_path,
182 10,
183 11,
184 12,
185 );
186 insert_store(
187 "schema_store_collision",
188 "StoreB",
189 canister_path,
190 13,
191 10,
192 14,
193 ); let err = canister
196 .validate()
197 .expect_err("memory-id collision must fail");
198
199 let rendered = err.to_string();
200 assert!(
201 rendered.contains("duplicate memory_id `10`"),
202 "expected duplicate memory-id error, got: {rendered}"
203 );
204 }
205
206 #[test]
207 fn validate_accepts_unique_memory_ids() {
208 let canister = insert_canister("schema_store_unique", "Canister");
209 let canister_path = "schema_store_unique::Canister";
210
211 insert_store("schema_store_unique", "StoreA", canister_path, 30, 31, 32);
212 insert_store("schema_store_unique", "StoreB", canister_path, 33, 34, 35);
213
214 canister.validate().expect("unique memory IDs should pass");
215 }
216
217 #[test]
218 fn validate_rejects_reserved_commit_memory_id() {
219 let canister = Canister::new(Def::new("schema_reserved_commit", "Canister"), 0, 255, 255);
220 schema_write().insert_node(SchemaNode::Canister(canister.clone()));
221
222 let err = canister
223 .validate()
224 .expect_err("reserved commit memory id must fail");
225
226 let rendered = err.to_string();
227 assert!(
228 rendered.contains("reserved for stable-structures internals"),
229 "expected reserved-id error, got: {rendered}"
230 );
231 }
232}