1mod arg;
7mod canister;
8mod def;
9mod entity;
10mod r#enum;
11mod field;
12mod index;
13mod item;
14mod list;
15mod map;
16mod newtype;
17mod primary_key;
18mod record;
19mod sanitizer;
20mod schema;
21mod set;
22mod store;
23mod tuple;
24mod r#type;
25mod validator;
26mod value;
27
28use crate::{
29 prelude::*,
30 visit::{Event, Visitor},
31};
32use std::any::Any;
33use thiserror::Error as ThisError;
34
35pub use arg::*;
36pub use canister::*;
37pub use def::*;
38pub use entity::*;
39pub use r#enum::*;
40pub use field::*;
41pub use index::*;
42pub use item::*;
43pub use list::*;
44pub use map::*;
45pub use newtype::*;
46pub use primary_key::*;
47pub use record::*;
48pub use sanitizer::*;
49pub use schema::*;
50pub use set::*;
51pub use store::*;
52pub use tuple::*;
53pub use r#type::*;
54pub use validator::*;
55pub use value::*;
56
57pub const APP_MEMORY_ID_MIN: u8 = 100;
58pub const APP_MEMORY_ID_MAX: u8 = 254;
59const RESERVED_INTERNAL_MEMORY_ID: u8 = u8::MAX;
60
61#[derive(Debug, ThisError)]
69pub enum NodeError {
70 #[error("{0} is an incorrect node type")]
71 IncorrectNodeType(String),
72
73 #[error("path not found: {0}")]
74 PathNotFound(String),
75}
76
77pub(crate) trait MacroNode: Any {
90 fn as_any(&self) -> &dyn Any;
91}
92
93pub(crate) trait ValidateNode {
101 fn validate(&self) -> Result<(), ErrorTree> {
102 Ok(())
103 }
104}
105
106pub(crate) trait VisitableNode: ValidateNode {
114 fn route_key(&self) -> String {
116 String::new()
117 }
118
119 fn accept<V: Visitor>(&self, visitor: &mut V) {
121 visitor.push(&self.route_key());
122 visitor.visit(self, Event::Enter);
123 self.drive(visitor);
124 visitor.visit(self, Event::Exit);
125 visitor.pop();
126 }
127
128 fn drive<V: Visitor>(&self, _: &mut V) {}
130}
131
132pub(crate) fn validate_memory_id_in_range(
134 errs: &mut ErrorTree,
135 label: &str,
136 memory_id: u8,
137 min: u8,
138 max: u8,
139) {
140 if !memory_id_is_in_range(memory_id, min, max) {
141 err!(errs, "{label} {memory_id} outside of range {min}-{max}");
142 }
143}
144
145pub(crate) fn validate_memory_id_not_reserved(errs: &mut ErrorTree, label: &str, memory_id: u8) {
147 if memory_id_is_reserved(memory_id) {
148 err!(
149 errs,
150 "{label} {memory_id} is reserved for stable-structures internals",
151 );
152 }
153}
154
155pub(crate) fn validate_app_memory_id(errs: &mut ErrorTree, label: &str, memory_id: u8) {
157 if !app_memory_id_is_valid(memory_id) {
158 err!(
159 errs,
160 "{label} {memory_id} outside of application memory range {APP_MEMORY_ID_MIN}-{APP_MEMORY_ID_MAX}",
161 );
162 }
163}
164
165#[must_use]
166pub const fn memory_id_is_in_range(memory_id: u8, min: u8, max: u8) -> bool {
167 memory_id >= min && memory_id <= max
168}
169
170#[must_use]
171pub const fn memory_id_is_reserved(memory_id: u8) -> bool {
172 memory_id == RESERVED_INTERNAL_MEMORY_ID
173}
174
175#[must_use]
176pub const fn app_memory_id_is_valid(memory_id: u8) -> bool {
177 memory_id >= APP_MEMORY_ID_MIN && memory_id <= APP_MEMORY_ID_MAX
178}
179
180pub(crate) fn validate_stable_key_segment(errs: &mut ErrorTree, label: &str, value: &str) {
181 if !stable_key_segment_is_canonical(value) {
182 err!(
183 errs,
184 "{label} `{value}` must use lowercase ASCII letters, digits, and underscores",
185 );
186 }
187}
188
189pub(crate) fn validate_stable_key(errs: &mut ErrorTree, label: &str, value: &str) {
190 if !stable_key_is_canonical(value) {
191 err!(
192 errs,
193 "{label} `{value}` must be canonical lowercase ASCII, must use dots as separators, must use underscores instead of hyphens, must end in .v1, and must not start with canic.",
194 );
195 }
196}
197
198#[must_use]
199pub fn stable_key_segment_is_canonical(value: &str) -> bool {
200 !value.is_empty()
201 && value
202 .bytes()
203 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
204}
205
206#[must_use]
207pub(crate) fn stable_key_is_canonical(value: &str) -> bool {
208 if value.starts_with("canic.") {
209 return false;
210 }
211
212 let mut saw_segment = false;
213 let mut last_segment = "";
214 for segment in value.split('.') {
215 if !stable_key_segment_is_canonical(segment) {
216 return false;
217 }
218 saw_segment = true;
219 last_segment = segment;
220 }
221
222 saw_segment && last_segment == "v1"
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn app_memory_id_policy_accepts_only_application_range() {
231 for memory_id in APP_MEMORY_ID_MIN..=APP_MEMORY_ID_MAX {
232 let mut errors = ErrorTree::new();
233 validate_app_memory_id(&mut errors, "memory_id", memory_id);
234 validate_memory_id_not_reserved(&mut errors, "memory_id", memory_id);
235 assert!(
236 errors.is_empty(),
237 "schema should accept app memory id {memory_id}: {errors}",
238 );
239 }
240
241 for memory_id in [0, APP_MEMORY_ID_MIN - 1] {
242 let mut errors = ErrorTree::new();
243 validate_app_memory_id(&mut errors, "memory_id", memory_id);
244 assert!(
245 !errors.is_empty(),
246 "schema should reject below-range app memory id {memory_id}",
247 );
248 }
249
250 let mut errors = ErrorTree::new();
251 validate_app_memory_id(&mut errors, "memory_id", u8::MAX);
252 validate_memory_id_not_reserved(&mut errors, "memory_id", u8::MAX);
253 let rendered = errors.to_string();
254 assert!(
255 rendered.contains("outside of application memory range 100-254"),
256 "reserved id should also fail the app range check: {rendered}",
257 );
258 assert!(
259 rendered.contains("reserved for stable-structures internals"),
260 "reserved id should fail closed explicitly: {rendered}",
261 );
262 }
263
264 #[test]
265 fn stable_key_segment_policy_is_canonical_ascii_only() {
266 for segment in ["db", "demo_rpg", "store_1", "v1"] {
267 assert!(stable_key_segment_is_canonical(segment));
268 }
269
270 for segment in ["", "Demo", "demo-rpg", "demo.rpg", "canic.owned"] {
271 assert!(!stable_key_segment_is_canonical(segment));
272 }
273 }
274
275 #[test]
276 fn full_stable_key_policy_rejects_reserved_and_malformed_keys() {
277 assert!(stable_key_is_canonical("icydb.demo_rpg.characters.data.v1"));
278
279 for key in [
280 "canic.demo_rpg.characters.data.v1",
281 "icydb.demo_rpg.characters.data",
282 "icydb.demo-rpg.characters.data.v1",
283 "icydb.demo_rpg..data.v1",
284 "icydb.Demo.characters.data.v1",
285 "icydb.demo_rpg.characters.data.v2",
286 ] {
287 assert!(!stable_key_is_canonical(key), "key should fail: {key}");
288 }
289 }
290}