Skip to main content

calimero_server_primitives/
validation.rs

1//! Input validation for server request types.
2//!
3//! This module provides comprehensive validation for all request types,
4//! checking payload sizes, string lengths, and format constraints.
5
6use thiserror::Error as ThisError;
7
8/// Maximum size for metadata fields (e.g., application metadata)
9pub const MAX_METADATA_SIZE: usize = 64 * 1024; // 64 KB
10
11/// Maximum size for initialization parameters
12pub const MAX_INIT_PARAMS_SIZE: usize = 1024 * 1024; // 1 MB
13
14/// Maximum length for protocol strings
15pub const MAX_PROTOCOL_LENGTH: usize = 64;
16
17/// Maximum length for package names
18pub const MAX_PACKAGE_NAME_LENGTH: usize = 128;
19
20/// Maximum length for version strings
21pub const MAX_VERSION_LENGTH: usize = 64;
22
23/// Maximum length for nonce strings (hex-encoded, 32 bytes = 64 chars)
24pub const MAX_NONCE_LENGTH: usize = 64;
25
26/// Maximum length for hash strings (hex-encoded, 32 bytes = 64 chars)
27pub const MAX_HASH_LENGTH: usize = 64;
28
29/// Maximum length for base64-encoded quote
30pub const MAX_QUOTE_B64_LENGTH: usize = 64 * 1024; // 64 KB
31
32/// Maximum length for URL strings
33pub const MAX_URL_LENGTH: usize = 2048;
34
35/// Maximum length for file paths
36pub const MAX_PATH_LENGTH: usize = 4096;
37
38/// Maximum number of capabilities in a single request
39pub const MAX_CAPABILITIES_COUNT: usize = 100;
40
41/// Maximum offset for pagination
42pub const MAX_PAGINATION_OFFSET: usize = 1_000_000;
43
44/// Maximum limit for pagination
45pub const MAX_PAGINATION_LIMIT: usize = 1000;
46
47/// Maximum length for context value keys
48pub const MAX_CONTEXT_KEY_LENGTH: usize = 1024;
49
50/// Maximum valid_for_blocks value (roughly 1 year at 1 block/second)
51pub const MAX_VALID_FOR_BLOCKS: u64 = 31_536_000;
52
53/// Maximum length for method names in execution requests
54pub const MAX_METHOD_NAME_LENGTH: usize = 256;
55
56/// Maximum size for JSON arguments in execution requests (10 MB)
57pub const MAX_ARGS_JSON_SIZE: usize = 10 * 1024 * 1024;
58
59/// Maximum number of substitute aliases in execution requests
60pub const MAX_SUBSTITUTE_ALIASES: usize = 100;
61
62/// Validation error types
63#[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
117/// Trait for validating request types
118pub trait Validate {
119    /// Validate the request and return a list of validation errors.
120    /// Returns an empty Vec if validation passes.
121    fn validate(&self) -> Vec<ValidationError>;
122
123    /// Validate and return the first error if any.
124    fn validate_first(&self) -> Result<(), ValidationError> {
125        self.validate().into_iter().next().map_or(Ok(()), Err)
126    }
127}
128
129/// Helper functions for common validations
130pub mod helpers {
131    use super::*;
132
133    /// Validate string length
134    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    /// Validate optional string length
151    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    /// Validate byte slice size
162    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    /// Validate hex string (must be valid hex and specific length)
179    ///
180    /// Uses character-based validation to avoid allocating a Vec for decoding.
181    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        // Validate hex characters without allocating
197        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    /// Validate optional hex string
208    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    /// Validate pagination offset
219    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    /// Validate pagination limit (must be > 0 and <= MAX_PAGINATION_LIMIT)
232    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    /// Validate collection size
252    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    /// Validate URL length
269    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    /// Validate method name (checks for empty, length, and control characters)
283    ///
284    /// Only minimal character restrictions are enforced here (control characters are rejected).
285    /// The OpenAPI spec does not define specific character constraints for method names, so
286    /// more specific validation is handled by the WASM execution layer at runtime.
287    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        // Check for control characters which are never valid in method names
301        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    /// Validate JSON value size using a recursive size estimator.
317    ///
318    /// This estimates the serialized size without allocating by walking the JSON tree.
319    /// The estimate uses a conservative 2x multiplier for strings to account for
320    /// JSON escape sequences (e.g., `"` becomes `\"`). This may overestimate but
321    /// ensures security against strings crafted to expand during serialization.
322    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    /// Recursively estimate the serialized size of a JSON value without allocating.
340    ///
341    /// Uses conservative estimates for strings (2x multiplier) to account for escape sequences.
342    /// This may overestimate the actual serialized size but prevents underestimation attacks
343    /// where strings with many escapable characters expand significantly during serialization.
344    fn estimate_json_size(value: &serde_json::Value) -> usize {
345        match value {
346            serde_json::Value::Null => 4, // "null"
347            serde_json::Value::Bool(b) => {
348                if *b {
349                    4
350                } else {
351                    5
352                }
353            } // "true" or "false"
354            serde_json::Value::Number(n) => n.to_string().len(), // Numbers vary in length
355            // Conservative: assume worst case where chars may need escaping (2x) + quotes
356            serde_json::Value::String(s) => s.len() * 2 + 2,
357            serde_json::Value::Array(arr) => {
358                // 2 for brackets, commas between elements
359                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                // 2 for braces, commas between entries, colons after keys
365                // Keys also use conservative 2x multiplier for escaping
366                let content_size: usize = obj
367                    .iter()
368                    .map(|(k, v)| k.len() * 2 + 2 + 1 + estimate_json_size(v)) // key*2 + quotes + colon + value
369                    .sum();
370                let comma_size = if obj.is_empty() { 0 } else { obj.len() - 1 };
371                2 + content_size + comma_size
372            }
373        }
374    }
375}