calimero_server_primitives/
validation.rs1use thiserror::Error as ThisError;
7
8pub const MAX_METADATA_SIZE: usize = 64 * 1024; pub const MAX_INIT_PARAMS_SIZE: usize = 1024 * 1024; pub const MAX_PROTOCOL_LENGTH: usize = 64;
16
17pub const MAX_PACKAGE_NAME_LENGTH: usize = 128;
19
20pub const MAX_VERSION_LENGTH: usize = 64;
22
23pub const MAX_NONCE_LENGTH: usize = 64;
25
26pub const MAX_HASH_LENGTH: usize = 64;
28
29pub const MAX_QUOTE_B64_LENGTH: usize = 64 * 1024; pub const MAX_URL_LENGTH: usize = 2048;
34
35pub const MAX_PATH_LENGTH: usize = 4096;
37
38pub const MAX_CAPABILITIES_COUNT: usize = 100;
40
41pub const MAX_PAGINATION_OFFSET: usize = 1_000_000;
43
44pub const MAX_PAGINATION_LIMIT: usize = 1000;
46
47pub const MAX_CONTEXT_KEY_LENGTH: usize = 1024;
49
50pub const MAX_VALID_FOR_BLOCKS: u64 = 31_536_000;
52
53pub const MAX_METHOD_NAME_LENGTH: usize = 256;
55
56pub const MAX_ARGS_JSON_SIZE: usize = 10 * 1024 * 1024;
58
59pub const MAX_SUBSTITUTE_ALIASES: usize = 100;
61
62#[derive(Clone, Debug, ThisError)]
64pub enum ValidationError {
65 #[error("Field '{field}' exceeds maximum length of {max} characters (got {actual})")]
66 StringTooLong {
67 field: &'static str,
68 max: usize,
69 actual: usize,
70 },
71
72 #[error("Field '{field}' exceeds maximum size of {max} bytes (got {actual})")]
73 PayloadTooLarge {
74 field: &'static str,
75 max: usize,
76 actual: usize,
77 },
78
79 #[error("Field '{field}' must be exactly {expected} characters (got {actual})")]
80 InvalidLength {
81 field: &'static str,
82 expected: usize,
83 actual: usize,
84 },
85
86 #[error("Field '{field}' contains invalid hex encoding: {reason}")]
87 InvalidHexEncoding { field: &'static str, reason: String },
88
89 #[error("Field '{field}' value {actual} exceeds maximum of {max}")]
90 ValueTooLarge {
91 field: &'static str,
92 max: u64,
93 actual: u64,
94 },
95
96 #[error("Field '{field}' value {actual} is below minimum of {min}")]
97 ValueTooSmall {
98 field: &'static str,
99 min: u64,
100 actual: u64,
101 },
102
103 #[error("Field '{field}' contains too many items: {actual} (max {max})")]
104 TooManyItems {
105 field: &'static str,
106 max: usize,
107 actual: usize,
108 },
109
110 #[error("Field '{field}' is required but was empty")]
111 EmptyField { field: &'static str },
112
113 #[error("Field '{field}' has invalid format: {reason}")]
114 InvalidFormat { field: &'static str, reason: String },
115}
116
117pub trait Validate {
119 fn validate(&self) -> Vec<ValidationError>;
122
123 fn validate_first(&self) -> Result<(), ValidationError> {
125 self.validate().into_iter().next().map_or(Ok(()), Err)
126 }
127}
128
129pub mod helpers {
131 use super::*;
132
133 pub fn validate_string_length(
135 value: &str,
136 field: &'static str,
137 max: usize,
138 ) -> Option<ValidationError> {
139 if value.len() > max {
140 Some(ValidationError::StringTooLong {
141 field,
142 max,
143 actual: value.len(),
144 })
145 } else {
146 None
147 }
148 }
149
150 pub fn validate_optional_string_length(
152 value: &Option<String>,
153 field: &'static str,
154 max: usize,
155 ) -> Option<ValidationError> {
156 value
157 .as_ref()
158 .and_then(|s| validate_string_length(s, field, max))
159 }
160
161 pub fn validate_bytes_size(
163 value: &[u8],
164 field: &'static str,
165 max: usize,
166 ) -> Option<ValidationError> {
167 if value.len() > max {
168 Some(ValidationError::PayloadTooLarge {
169 field,
170 max,
171 actual: value.len(),
172 })
173 } else {
174 None
175 }
176 }
177
178 pub fn validate_hex_string(
182 value: &str,
183 field: &'static str,
184 expected_bytes: usize,
185 ) -> Option<ValidationError> {
186 let expected_chars = expected_bytes * 2;
187
188 if value.len() != expected_chars {
189 return Some(ValidationError::InvalidLength {
190 field,
191 expected: expected_chars,
192 actual: value.len(),
193 });
194 }
195
196 if !value.chars().all(|c| c.is_ascii_hexdigit()) {
198 return Some(ValidationError::InvalidHexEncoding {
199 field,
200 reason: "contains non-hexadecimal characters".to_owned(),
201 });
202 }
203
204 None
205 }
206
207 pub fn validate_optional_hex_string(
209 value: &Option<String>,
210 field: &'static str,
211 expected_bytes: usize,
212 ) -> Option<ValidationError> {
213 value
214 .as_ref()
215 .and_then(|s| validate_hex_string(s, field, expected_bytes))
216 }
217
218 pub fn validate_offset(value: usize, field: &'static str) -> Option<ValidationError> {
220 if value > MAX_PAGINATION_OFFSET {
221 Some(ValidationError::ValueTooLarge {
222 field,
223 max: MAX_PAGINATION_OFFSET as u64,
224 actual: value as u64,
225 })
226 } else {
227 None
228 }
229 }
230
231 pub fn validate_limit(value: usize, field: &'static str) -> Option<ValidationError> {
233 if value == 0 {
234 return Some(ValidationError::ValueTooSmall {
235 field,
236 min: 1,
237 actual: 0,
238 });
239 }
240 if value > MAX_PAGINATION_LIMIT {
241 Some(ValidationError::ValueTooLarge {
242 field,
243 max: MAX_PAGINATION_LIMIT as u64,
244 actual: value as u64,
245 })
246 } else {
247 None
248 }
249 }
250
251 pub fn validate_collection_size<T>(
253 value: &[T],
254 field: &'static str,
255 max: usize,
256 ) -> Option<ValidationError> {
257 if value.len() > max {
258 Some(ValidationError::TooManyItems {
259 field,
260 max,
261 actual: value.len(),
262 })
263 } else {
264 None
265 }
266 }
267
268 pub fn validate_url(value: &url::Url, field: &'static str) -> Option<ValidationError> {
270 let url_str = value.as_str();
271 if url_str.len() > MAX_URL_LENGTH {
272 Some(ValidationError::StringTooLong {
273 field,
274 max: MAX_URL_LENGTH,
275 actual: url_str.len(),
276 })
277 } else {
278 None
279 }
280 }
281
282 pub fn validate_method_name(value: &str, field: &'static str) -> Option<ValidationError> {
288 if value.is_empty() {
289 return Some(ValidationError::EmptyField { field });
290 }
291
292 if value.len() > MAX_METHOD_NAME_LENGTH {
293 return Some(ValidationError::StringTooLong {
294 field,
295 max: MAX_METHOD_NAME_LENGTH,
296 actual: value.len(),
297 });
298 }
299
300 for c in value.chars() {
302 if c.is_ascii_control() {
303 return Some(ValidationError::InvalidFormat {
304 field,
305 reason: format!(
306 "contains control character '{}' which is not allowed",
307 c.escape_default()
308 ),
309 });
310 }
311 }
312
313 None
314 }
315
316 pub fn validate_json_size(
323 value: &serde_json::Value,
324 field: &'static str,
325 max: usize,
326 ) -> Option<ValidationError> {
327 let size = estimate_json_size(value);
328 if size > max {
329 Some(ValidationError::PayloadTooLarge {
330 field,
331 max,
332 actual: size,
333 })
334 } else {
335 None
336 }
337 }
338
339 fn estimate_json_size(value: &serde_json::Value) -> usize {
345 match value {
346 serde_json::Value::Null => 4, serde_json::Value::Bool(b) => {
348 if *b {
349 4
350 } else {
351 5
352 }
353 } serde_json::Value::Number(n) => n.to_string().len(), serde_json::Value::String(s) => s.len() * 2 + 2,
357 serde_json::Value::Array(arr) => {
358 let content_size: usize = arr.iter().map(estimate_json_size).sum();
360 let comma_size = if arr.is_empty() { 0 } else { arr.len() - 1 };
361 2 + content_size + comma_size
362 }
363 serde_json::Value::Object(obj) => {
364 let content_size: usize = obj
367 .iter()
368 .map(|(k, v)| k.len() * 2 + 2 + 1 + estimate_json_size(v)) .sum();
370 let comma_size = if obj.is_empty() { 0 } else { obj.len() - 1 };
371 2 + content_size + comma_size
372 }
373 }
374 }
375}