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
99 errs.result()
100 }
101}
102
103fn assert_unique_memory_id(
104 memory_id: u8,
105 slot: String,
106 canister_path: &str,
107 seen_ids: &mut BTreeMap<u8, String>,
108 errs: &mut ErrorTree,
109) {
110 if let Some(existing) = seen_ids.get(&memory_id) {
111 err!(
112 errs,
113 "duplicate memory_id `{}` used in canister `{}`: {} conflicts with {}",
114 memory_id,
115 canister_path,
116 existing,
117 slot
118 );
119 } else {
120 seen_ids.insert(memory_id, slot);
121 }
122}
123
124impl VisitableNode for Canister {
125 fn route_key(&self) -> String {
126 self.def().path()
127 }
128}
129
130#[cfg(test)]
135mod tests {
136 use crate::build::schema_write;
137
138 use super::*;
139
140 fn insert_canister(path_module: &'static str, ident: &'static str) -> Canister {
141 let canister = Canister::new(Def::new(path_module, ident, None), 0, 255, 254);
142 schema_write().insert_node(SchemaNode::Canister(canister.clone()));
143
144 canister
145 }
146
147 fn insert_store(
148 path_module: &'static str,
149 ident: &'static str,
150 canister_path: &'static str,
151 data_memory_id: u8,
152 index_memory_id: u8,
153 ) {
154 schema_write().insert_node(SchemaNode::Store(Store::new(
155 Def::new(path_module, ident, None),
156 ident,
157 canister_path,
158 data_memory_id,
159 index_memory_id,
160 )));
161 }
162
163 #[test]
164 fn validate_rejects_memory_id_collision_between_stores() {
165 let canister = insert_canister("schema_store_collision", "Canister");
166 let canister_path = "schema_store_collision::Canister";
167
168 insert_store("schema_store_collision", "StoreA", canister_path, 10, 11);
169 insert_store("schema_store_collision", "StoreB", canister_path, 12, 10); let err = canister
172 .validate()
173 .expect_err("memory-id collision must fail");
174
175 let rendered = err.to_string();
176 assert!(
177 rendered.contains("duplicate memory_id `10`"),
178 "expected duplicate memory-id error, got: {rendered}"
179 );
180 }
181
182 #[test]
183 fn validate_accepts_unique_memory_ids() {
184 let canister = insert_canister("schema_store_unique", "Canister");
185 let canister_path = "schema_store_unique::Canister";
186
187 insert_store("schema_store_unique", "StoreA", canister_path, 30, 31);
188 insert_store("schema_store_unique", "StoreB", canister_path, 32, 33);
189
190 canister.validate().expect("unique memory IDs should pass");
191 }
192
193 #[test]
194 fn validate_rejects_reserved_commit_memory_id() {
195 let canister = Canister::new(
196 Def::new("schema_reserved_commit", "Canister", None),
197 0,
198 255,
199 255,
200 );
201 schema_write().insert_node(SchemaNode::Canister(canister.clone()));
202
203 let err = canister
204 .validate()
205 .expect_err("reserved commit memory id must fail");
206
207 let rendered = err.to_string();
208 assert!(
209 rendered.contains("reserved for stable-structures internals"),
210 "expected reserved-id error, got: {rendered}"
211 );
212 }
213}