1#[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;
16use std::str::FromStr;
17
18pub type MigrationPlanConstructor =
20 fn() -> Result<icydb_schema::SchemaMigrationPlan, icydb_schema::SchemaContractError>;
21
22#[doc(hidden)]
24pub fn migration_literal_from_text(
25 kind: &str,
26 value: &str,
27) -> Result<icydb_schema::ScalarLiteral, icydb_schema::SchemaContractError> {
28 use icydb_schema::{
29 Account, Blob, Date, Decimal, Duration, IntBig, NatBig, Principal, ScalarLiteral,
30 Subaccount, Timestamp, Ulid,
31 };
32
33 let invalid = || icydb_schema::SchemaContractError::InvalidLiteral;
34 match kind {
35 "account" => Account::from_str(value)
36 .map(ScalarLiteral::Account)
37 .map_err(|_| invalid()),
38 "blob" => decode_migration_hex(value)
39 .map(Blob::from)
40 .map(ScalarLiteral::Blob),
41 "date" => Date::parse(value)
42 .map(ScalarLiteral::Date)
43 .ok_or_else(invalid),
44 "decimal" => Decimal::from_str(value)
45 .map(ScalarLiteral::Decimal)
46 .map_err(|_| invalid()),
47 "duration" => Duration::parse_flexible(value)
48 .map(ScalarLiteral::Duration)
49 .map_err(|_| invalid()),
50 "int_big" => IntBig::from_str(value)
51 .map(ScalarLiteral::IntBig)
52 .map_err(|_| invalid()),
53 "nat_big" => NatBig::from_str(value)
54 .map(ScalarLiteral::NatBig)
55 .map_err(|_| invalid()),
56 "principal" => Principal::from_str(value)
57 .map(ScalarLiteral::Principal)
58 .map_err(|_| invalid()),
59 "subaccount" => {
60 let bytes = decode_migration_hex(value)?;
61 let bytes: [u8; 32] = bytes.try_into().map_err(|_| invalid())?;
62 Ok(ScalarLiteral::Subaccount(Subaccount::from_array(bytes)))
63 }
64 "timestamp" => Timestamp::parse_flexible(value)
65 .map(ScalarLiteral::Timestamp)
66 .map_err(|_| invalid()),
67 "ulid" => Ulid::from_str(value)
68 .map(ScalarLiteral::Ulid)
69 .map_err(|_| invalid()),
70 _ => Err(invalid()),
71 }
72}
73
74fn decode_migration_hex(value: &str) -> Result<Vec<u8>, icydb_schema::SchemaContractError> {
75 if !value.len().is_multiple_of(2) {
76 return Err(icydb_schema::SchemaContractError::InvalidLiteral);
77 }
78 value
79 .as_bytes()
80 .chunks_exact(2)
81 .map(|pair| {
82 let high = decode_hex_nibble(pair[0])?;
83 let low = decode_hex_nibble(pair[1])?;
84 Ok((high << 4) | low)
85 })
86 .collect()
87}
88
89const fn decode_hex_nibble(value: u8) -> Result<u8, icydb_schema::SchemaContractError> {
90 match value {
91 b'0'..=b'9' => Ok(value - b'0'),
92 b'a'..=b'f' => Ok(value - b'a' + 10),
93 b'A'..=b'F' => Ok(value - b'A' + 10),
94 _ => Err(icydb_schema::SchemaContractError::InvalidLiteral),
95 }
96}
97
98#[derive(Clone, Debug, Serialize)]
103pub struct Canister {
104 def: Def,
105 memory_namespace: &'static str,
106 memory_min: u8,
107 memory_max: u8,
108 commit_memory_id: u8,
109 integrity_progress_memory_id: u8,
110 #[serde(skip)]
111 migration_plan: Option<MigrationPlanConstructor>,
112}
113
114impl Canister {
115 #[must_use]
116 pub const fn new(
117 def: Def,
118 memory_namespace: &'static str,
119 memory_min: u8,
120 memory_max: u8,
121 commit_memory_id: u8,
122 integrity_progress_memory_id: u8,
123 migration_plan: Option<MigrationPlanConstructor>,
124 ) -> Self {
125 Self {
126 def,
127 memory_namespace,
128 memory_min,
129 memory_max,
130 commit_memory_id,
131 integrity_progress_memory_id,
132 migration_plan,
133 }
134 }
135
136 #[must_use]
137 pub const fn def(&self) -> &Def {
138 &self.def
139 }
140
141 #[must_use]
142 pub const fn memory_namespace(&self) -> &'static str {
143 self.memory_namespace
144 }
145
146 #[must_use]
147 pub const fn memory_min(&self) -> u8 {
148 self.memory_min
149 }
150
151 #[must_use]
152 pub const fn memory_max(&self) -> u8 {
153 self.memory_max
154 }
155
156 #[must_use]
157 pub const fn commit_memory_id(&self) -> u8 {
158 self.commit_memory_id
159 }
160
161 #[must_use]
162 pub const fn integrity_progress_memory_id(&self) -> u8 {
163 self.integrity_progress_memory_id
164 }
165
166 pub fn migration_plan(
172 &self,
173 ) -> Result<Option<icydb_schema::SchemaMigrationPlan>, icydb_schema::SchemaContractError> {
174 self.migration_plan
175 .map(|constructor| constructor())
176 .transpose()
177 }
178
179 #[must_use]
180 pub fn commit_stable_key(&self) -> String {
181 stable_memory_key(self.memory_namespace(), "commit", "control")
182 }
183
184 #[must_use]
185 pub fn integrity_progress_stable_key(&self) -> String {
186 stable_memory_key(self.memory_namespace(), "integrity", "progress")
187 }
188
189 fn validate_declared_memory_contract(&self, errs: &mut ErrorTree) {
190 validate_stable_key_segment(errs, "canister memory_namespace", self.memory_namespace());
191 validate_memory_id_in_range(
192 errs,
193 "commit_memory_id",
194 self.commit_memory_id(),
195 self.memory_min(),
196 self.memory_max(),
197 );
198 validate_app_memory_id(errs, "commit_memory_id", self.commit_memory_id());
199 validate_memory_id_not_reserved(errs, "commit_memory_id", self.commit_memory_id());
200 validate_stable_key(errs, "commit stable key", &self.commit_stable_key());
201 validate_memory_id_in_range(
202 errs,
203 "integrity_progress_memory_id",
204 self.integrity_progress_memory_id(),
205 self.memory_min(),
206 self.memory_max(),
207 );
208 validate_app_memory_id(
209 errs,
210 "integrity_progress_memory_id",
211 self.integrity_progress_memory_id(),
212 );
213 validate_memory_id_not_reserved(
214 errs,
215 "integrity_progress_memory_id",
216 self.integrity_progress_memory_id(),
217 );
218 validate_stable_key(
219 errs,
220 "integrity progress stable key",
221 &self.integrity_progress_stable_key(),
222 );
223 }
224
225 fn register_store_allocations(
226 &self,
227 canister_path: &str,
228 seen_ids: &mut BTreeMap<u8, (String, String)>,
229 seen_keys: &mut BTreeMap<String, (u8, String)>,
230 errs: &mut ErrorTree,
231 ) {
232 let schema = schema_read();
233 for (path, store) in schema.filter_nodes::<Store>(|node| node.canister() == canister_path) {
234 if !matches!(store.storage(), StoreStorage::Journaled(_)) {
235 continue;
236 }
237 for (allocation, role) in [
238 (
239 store.stable_data_allocation(self.memory_namespace()),
240 "data",
241 ),
242 (
243 store.stable_index_allocation(self.memory_namespace()),
244 "index",
245 ),
246 (
247 store.stable_schema_allocation(self.memory_namespace()),
248 "schema",
249 ),
250 ] {
251 assert_unique_memory_allocation(
252 allocation.memory_id(),
253 allocation.stable_key().to_string(),
254 format!("Store `{path}`.{role}_memory"),
255 canister_path,
256 seen_ids,
257 seen_keys,
258 errs,
259 );
260 }
261 if store.is_journaled_storage() {
262 let allocation = store.journal_allocation(self.memory_namespace());
263 assert_unique_memory_allocation(
264 allocation.memory_id(),
265 allocation.stable_key().to_string(),
266 format!("Store `{path}`.journal_memory"),
267 canister_path,
268 seen_ids,
269 seen_keys,
270 errs,
271 );
272 }
273 }
274 }
275}
276
277impl MacroNode for Canister {
278 fn as_any(&self) -> &dyn std::any::Any {
279 self
280 }
281}
282
283impl ValidateNode for Canister {
284 fn validate(&self) -> Result<(), ErrorTree> {
285 let mut errs = ErrorTree::new();
286
287 let canister_path = self.def().path();
288 let mut seen_ids = BTreeMap::<u8, (String, String)>::new();
289 let mut seen_keys = BTreeMap::<String, (u8, String)>::new();
290
291 self.validate_declared_memory_contract(&mut errs);
292
293 assert_unique_memory_allocation(
294 self.commit_memory_id(),
295 self.commit_stable_key(),
296 format!("Canister `{}`.commit_memory", self.def().path()),
297 &canister_path,
298 &mut seen_ids,
299 &mut seen_keys,
300 &mut errs,
301 );
302 assert_unique_memory_allocation(
303 self.integrity_progress_memory_id(),
304 self.integrity_progress_stable_key(),
305 format!("Canister `{}`.integrity_progress_memory", self.def().path()),
306 &canister_path,
307 &mut seen_ids,
308 &mut seen_keys,
309 &mut errs,
310 );
311 self.register_store_allocations(&canister_path, &mut seen_ids, &mut seen_keys, &mut errs);
312
313 errs.result()
314 }
315}
316
317fn assert_unique_memory_allocation(
318 memory_id: u8,
319 stable_key: String,
320 slot: String,
321 canister_path: &str,
322 seen_ids: &mut BTreeMap<u8, (String, String)>,
323 seen_keys: &mut BTreeMap<String, (u8, String)>,
324 errs: &mut ErrorTree,
325) {
326 if let Some((existing_key, existing_slot)) = seen_ids.get(&memory_id) {
327 err!(
328 errs,
329 "duplicate memory_id `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
330 memory_id,
331 canister_path,
332 existing_slot,
333 existing_key,
334 slot,
335 stable_key,
336 );
337 } else {
338 seen_ids.insert(memory_id, (stable_key.clone(), slot.clone()));
339 }
340
341 if let Some((existing_id, existing_slot)) = seen_keys.get(&stable_key) {
342 err!(
343 errs,
344 "duplicate stable_key `{}` used in canister `{}`: {} ({}) conflicts with {} ({})",
345 stable_key,
346 canister_path,
347 existing_slot,
348 existing_id,
349 slot,
350 memory_id,
351 );
352 } else {
353 seen_keys.insert(stable_key, (memory_id, slot));
354 }
355}
356
357impl VisitableNode for Canister {
358 fn route_key(&self) -> String {
359 self.def().path()
360 }
361}