chie_shared/types/
validation.rs

1//! Validation types and utilities for CHIE Protocol.
2
3use super::core::*;
4use std::fmt;
5
6/// Validation error for proof and content data.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ValidationError {
9    /// Public key has wrong length.
10    InvalidPublicKeyLength { expected: usize, actual: usize },
11    /// Signature has wrong length.
12    InvalidSignatureLength { expected: usize, actual: usize },
13    /// Nonce has wrong length.
14    InvalidNonceLength { expected: usize, actual: usize },
15    /// Hash has wrong length.
16    InvalidHashLength { expected: usize, actual: usize },
17    /// Timestamp is in the future.
18    TimestampInFuture { timestamp_ms: i64, now_ms: i64 },
19    /// Timestamp is too old.
20    TimestampTooOld {
21        timestamp_ms: i64,
22        now_ms: i64,
23        tolerance_ms: i64,
24    },
25    /// Start timestamp is after end timestamp.
26    InvalidTimestampOrder { start_ms: i64, end_ms: i64 },
27    /// Latency is suspiciously low.
28    LatencyTooLow { latency_ms: u32, min_ms: u32 },
29    /// Latency is unreasonably high.
30    LatencyTooHigh { latency_ms: u32, max_ms: u32 },
31    /// Latency doesn't match timestamps.
32    LatencyMismatch {
33        calculated_ms: i64,
34        reported_ms: u32,
35    },
36    /// Bytes transferred exceeds maximum.
37    BytesExceedMax { bytes: u64, max: u64 },
38    /// Provider and requester are the same.
39    SelfTransfer,
40    /// Content CID is empty.
41    EmptyCid,
42    /// Content size out of bounds.
43    ContentSizeOutOfBounds { size: u64, min: u64, max: u64 },
44    /// Title too long.
45    TitleTooLong { length: usize, max: usize },
46    /// Description too long.
47    DescriptionTooLong { length: usize, max: usize },
48    /// Too many tags.
49    TooManyTags { count: usize, max: usize },
50    /// Tag too long.
51    TagTooLong {
52        tag: String,
53        length: usize,
54        max: usize,
55    },
56}
57
58impl fmt::Display for ValidationError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::InvalidPublicKeyLength { expected, actual } => {
62                write!(
63                    f,
64                    "Invalid public key length: expected {expected}, got {actual}"
65                )
66            }
67            Self::InvalidSignatureLength { expected, actual } => {
68                write!(
69                    f,
70                    "Invalid signature length: expected {expected}, got {actual}"
71                )
72            }
73            Self::InvalidNonceLength { expected, actual } => {
74                write!(f, "Invalid nonce length: expected {expected}, got {actual}")
75            }
76            Self::InvalidHashLength { expected, actual } => {
77                write!(f, "Invalid hash length: expected {expected}, got {actual}")
78            }
79            Self::TimestampInFuture {
80                timestamp_ms,
81                now_ms,
82            } => {
83                write!(
84                    f,
85                    "Timestamp {timestamp_ms} is in the future (now: {now_ms})"
86                )
87            }
88            Self::TimestampTooOld {
89                timestamp_ms,
90                now_ms,
91                tolerance_ms,
92            } => {
93                write!(
94                    f,
95                    "Timestamp {timestamp_ms} is too old (now: {now_ms}, tolerance: {tolerance_ms}ms)"
96                )
97            }
98            Self::InvalidTimestampOrder { start_ms, end_ms } => {
99                write!(
100                    f,
101                    "Start timestamp {start_ms} is after end timestamp {end_ms}"
102                )
103            }
104            Self::LatencyTooLow { latency_ms, min_ms } => {
105                write!(f, "Latency {latency_ms}ms is below minimum {min_ms}ms")
106            }
107            Self::LatencyTooHigh { latency_ms, max_ms } => {
108                write!(f, "Latency {latency_ms}ms exceeds maximum {max_ms}ms")
109            }
110            Self::LatencyMismatch {
111                calculated_ms,
112                reported_ms,
113            } => {
114                write!(
115                    f,
116                    "Reported latency {reported_ms}ms doesn't match calculated {calculated_ms}ms"
117                )
118            }
119            Self::BytesExceedMax { bytes, max } => {
120                write!(f, "Bytes transferred {bytes} exceeds maximum {max}")
121            }
122            Self::SelfTransfer => write!(f, "Provider and requester cannot be the same"),
123            Self::EmptyCid => write!(f, "Content CID cannot be empty"),
124            Self::ContentSizeOutOfBounds { size, min, max } => {
125                write!(f, "Content size {size} is out of bounds [{min}, {max}]")
126            }
127            Self::TitleTooLong { length, max } => {
128                write!(f, "Title length {length} exceeds maximum {max}")
129            }
130            Self::DescriptionTooLong { length, max } => {
131                write!(f, "Description length {length} exceeds maximum {max}")
132            }
133            Self::TooManyTags { count, max } => {
134                write!(f, "Tag count {count} exceeds maximum {max}")
135            }
136            Self::TagTooLong { tag, length, max } => {
137                write!(f, "Tag '{tag}' length {length} exceeds maximum {max}")
138            }
139        }
140    }
141}
142
143impl std::error::Error for ValidationError {}
144
145/// Validation helpers for common operations.
146pub mod helpers {
147    use super::*;
148
149    /// Validate a peer ID format.
150    ///
151    /// # Errors
152    ///
153    /// Returns `ValidationError::EmptyCid` if peer ID is empty or invalid
154    pub fn validate_peer_id(peer_id: &str) -> Result<(), ValidationError> {
155        if peer_id.is_empty() {
156            return Err(ValidationError::EmptyCid); // Reusing error type
157        }
158        if !crate::utils::is_valid_peer_id(peer_id) {
159            return Err(ValidationError::EmptyCid); // Could add specific error
160        }
161        Ok(())
162    }
163
164    /// Validate a content CID format.
165    ///
166    /// # Errors
167    ///
168    /// Returns `ValidationError::EmptyCid` if CID is empty or invalid
169    pub fn validate_cid(cid: &str) -> Result<(), ValidationError> {
170        if cid.is_empty() {
171            return Err(ValidationError::EmptyCid);
172        }
173        if !crate::utils::is_valid_cid(cid) {
174            return Err(ValidationError::EmptyCid);
175        }
176        Ok(())
177    }
178
179    /// Validate tag length and format.
180    ///
181    /// # Errors
182    ///
183    /// Returns `ValidationError::TagTooLong` if tag exceeds maximum length
184    pub fn validate_tag(tag: &str) -> Result<(), ValidationError> {
185        if tag.len() > MAX_TAG_LENGTH {
186            return Err(ValidationError::TagTooLong {
187                tag: tag.to_string(),
188                length: tag.len(),
189                max: MAX_TAG_LENGTH,
190            });
191        }
192        Ok(())
193    }
194
195    /// Validate all tags in a collection.
196    ///
197    /// # Errors
198    ///
199    /// Returns `Vec<ValidationError>` with all validation errors found
200    pub fn validate_tags(tags: &[String]) -> Result<(), Vec<ValidationError>> {
201        let mut errors = Vec::new();
202
203        if tags.len() > MAX_TAGS_COUNT {
204            errors.push(ValidationError::TooManyTags {
205                count: tags.len(),
206                max: MAX_TAGS_COUNT,
207            });
208        }
209
210        for tag in tags {
211            if let Err(e) = validate_tag(tag) {
212                errors.push(e);
213            }
214        }
215
216        if errors.is_empty() {
217            Ok(())
218        } else {
219            Err(errors)
220        }
221    }
222
223    /// Validate content size bounds.
224    ///
225    /// # Errors
226    ///
227    /// Returns `ValidationError::ContentSizeOutOfBounds` if size is outside allowed range
228    pub fn validate_content_size(size: u64) -> Result<(), ValidationError> {
229        if !(MIN_CONTENT_SIZE..=MAX_CONTENT_SIZE).contains(&size) {
230            return Err(ValidationError::ContentSizeOutOfBounds {
231                size,
232                min: MIN_CONTENT_SIZE,
233                max: MAX_CONTENT_SIZE,
234            });
235        }
236        Ok(())
237    }
238
239    /// Validate title length.
240    ///
241    /// # Errors
242    ///
243    /// Returns `ValidationError::TitleTooLong` if title exceeds maximum length
244    pub fn validate_title(title: &str) -> Result<(), ValidationError> {
245        if title.len() > MAX_TITLE_LENGTH {
246            return Err(ValidationError::TitleTooLong {
247                length: title.len(),
248                max: MAX_TITLE_LENGTH,
249            });
250        }
251        Ok(())
252    }
253
254    /// Validate description length.
255    ///
256    /// # Errors
257    ///
258    /// Returns `ValidationError::DescriptionTooLong` if description exceeds maximum length
259    pub fn validate_description(description: &str) -> Result<(), ValidationError> {
260        if description.len() > MAX_DESCRIPTION_LENGTH {
261            return Err(ValidationError::DescriptionTooLong {
262                length: description.len(),
263                max: MAX_DESCRIPTION_LENGTH,
264            });
265        }
266        Ok(())
267    }
268
269    /// Batch validate multiple values, collecting all errors.
270    pub fn validate_all<T, F>(items: &[T], validator: F) -> Result<(), Vec<ValidationError>>
271    where
272        F: Fn(&T) -> Result<(), ValidationError>,
273    {
274        let errors: Vec<ValidationError> = items
275            .iter()
276            .filter_map(|item| validator(item).err())
277            .collect();
278
279        if errors.is_empty() {
280            Ok(())
281        } else {
282            Err(errors)
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_validation_error_display() {
293        let err = ValidationError::InvalidPublicKeyLength {
294            expected: 32,
295            actual: 16,
296        };
297        assert!(err.to_string().contains("Invalid public key length"));
298
299        let err = ValidationError::SelfTransfer;
300        assert!(err.to_string().contains("cannot be the same"));
301
302        let err = ValidationError::EmptyCid;
303        assert!(err.to_string().contains("cannot be empty"));
304    }
305}