1use crate::error::{CoreError, CoreResult, ErrorContext};
7use std::collections::HashMap;
8use std::fmt;
9use std::time::{Duration, Instant};
10
11#[cfg(feature = "regex")]
12use regex;
13
14const MAX_DIMENSIONS: usize = 5;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19pub enum ValidationLevel {
20 Development,
22 Testing,
24 Staging,
26 Production,
28}
29
30#[derive(Debug, Clone)]
32pub struct ValidationContext {
33 pub level: ValidationLevel,
35 pub timeout: Duration,
37 pub collect_metrics: bool,
39 pub custom_rules: HashMap<String, String>,
41}
42
43impl Default for ValidationContext {
44 fn default() -> Self {
45 Self {
46 level: ValidationLevel::Production,
47 timeout: Duration::from_millis(100), collect_metrics: true,
49 custom_rules: HashMap::new(),
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct ValidationResult {
57 pub is_valid: bool,
59 pub errors: Vec<ValidationError>,
61 pub warnings: Vec<String>,
63 pub metrics: ValidationMetrics,
65}
66
67#[derive(Debug, Clone)]
69pub struct ValidationError {
70 pub code: String,
72 pub message: String,
74 pub field: Option<String>,
76 pub suggestion: Option<String>,
78 pub severity: ValidationSeverity,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
84pub enum ValidationSeverity {
85 Info,
87 Warning,
89 Error,
91 Critical,
93}
94
95#[derive(Debug, Clone)]
97pub struct ValidationMetrics {
98 pub duration: Duration,
100 pub rules_checked: usize,
102 pub values_validated: usize,
104 pub memory_used: usize,
106 pub timed_out: bool,
108}
109
110impl Default for ValidationMetrics {
111 fn default() -> Self {
112 Self {
113 duration: Duration::ZERO,
114 rules_checked: 0,
115 values_validated: 0,
116 memory_used: 0,
117 timed_out: false,
118 }
119 }
120}
121
122pub struct ProductionValidator {
124 context: ValidationContext,
126 cache: HashMap<String, ValidationResult>,
128 metrics_collector: Option<Box<dyn ValidationMetricsCollector + Send + Sync>>,
130}
131
132pub trait ValidationMetricsCollector {
134 fn record_validation(&mut self, result: &ValidationResult);
136
137 fn get_metrics(&self) -> ValidationSummary;
139
140 fn clear(&mut self);
142}
143
144#[derive(Debug, Clone)]
146pub struct ValidationSummary {
147 pub total_validations: usize,
149 pub total_duration: Duration,
151 pub average_duration: Duration,
153 pub success_rate: f64,
155 pub commonerrors: HashMap<String, usize>,
157}
158
159impl ProductionValidator {
160 pub fn new() -> Self {
162 Self {
163 context: ValidationContext::default(),
164 cache: HashMap::new(),
165 metrics_collector: None,
166 }
167 }
168
169 pub fn with_context(context: ValidationContext) -> Self {
171 Self {
172 context,
173 cache: HashMap::new(),
174 metrics_collector: None,
175 }
176 }
177
178 pub fn with_metrics_collector(
180 mut self,
181 collector: Box<dyn ValidationMetricsCollector + Send + Sync>,
182 ) -> Self {
183 self.metrics_collector = Some(collector);
184 self
185 }
186
187 pub fn validate_numeric<T>(
189 &mut self,
190 value: T,
191 constraints: &NumericConstraints<T>,
192 ) -> ValidationResult
193 where
194 T: PartialOrd + Copy + fmt::Debug + fmt::Display,
195 {
196 let start_time = Instant::now();
197 let mut result = ValidationResult {
198 is_valid: true,
199 errors: Vec::new(),
200 warnings: Vec::new(),
201 metrics: ValidationMetrics::default(),
202 };
203
204 if start_time.elapsed() > self.context.timeout {
206 result.metrics.timed_out = true;
207 result.is_valid = false;
208 result.errors.push(ValidationError {
209 code: "VALIDATION_TIMEOUT".to_string(),
210 message: "Validation timed out".to_string(),
211 field: None,
212 suggestion: Some("Increase validation timeout or simplify constraints".to_string()),
213 severity: ValidationSeverity::Error,
214 });
215 return result;
216 }
217
218 let mut rules_checked = 0;
219
220 if let Some(min) = constraints.min {
222 rules_checked += 1;
223 if value < min {
224 result.is_valid = false;
225 result.errors.push(ValidationError {
226 code: "VALUE_TOO_SMALL".to_string(),
227 message: format!("Value {value} is below minimum {min}"),
228 field: constraints.fieldname.clone(),
229 suggestion: Some(format!("{min}")),
230 severity: ValidationSeverity::Error,
231 });
232 }
233 }
234
235 if let Some(max) = constraints.max {
236 rules_checked += 1;
237 if value > max {
238 result.is_valid = false;
239 result.errors.push(ValidationError {
240 code: "VALUE_TOO_LARGE".to_string(),
241 message: format!("Value {value} exceeds maximum {max}"),
242 field: constraints.fieldname.clone(),
243 suggestion: Some(format!("{max}")),
244 severity: ValidationSeverity::Error,
245 });
246 }
247 }
248
249 for rule in &constraints.custom_rules {
251 rules_checked += 1;
252 if let Err(error) = rule.validate(value) {
253 match constraints.allow_custom_rule_failures {
254 true => result.warnings.push(error),
255 false => {
256 result.is_valid = false;
257 result.errors.push(ValidationError {
258 code: "CUSTOM_RULE_FAILED".to_string(),
259 message: error,
260 field: constraints.fieldname.clone(),
261 suggestion: Some("Check custom validation rules".to_string()),
262 severity: ValidationSeverity::Error,
263 });
264 }
265 }
266 }
267 }
268
269 if rules_checked > 10 {
271 result
272 .warnings
273 .push("High number of validation rules may impact performance".to_string());
274 }
275
276 result.metrics = ValidationMetrics {
277 duration: start_time.elapsed(),
278 rules_checked,
279 values_validated: 1,
280 memory_used: std::mem::size_of::<T>(),
281 timed_out: false,
282 };
283
284 if let Some(collector) = &mut self.metrics_collector {
286 collector.record_validation(&result);
287 }
288
289 result
290 }
291
292 pub fn validate_collection<T, I>(
294 &mut self,
295 collection: I,
296 constraints: &CollectionConstraints<T>,
297 ) -> ValidationResult
298 where
299 T: fmt::Debug,
300 I: IntoIterator<Item = T>,
301 I::IntoIter: ExactSizeIterator,
302 {
303 let start_time = Instant::now();
304 let mut result = ValidationResult {
305 is_valid: true,
306 errors: Vec::new(),
307 warnings: Vec::new(),
308 metrics: ValidationMetrics::default(),
309 };
310
311 let iter = collection.into_iter();
312 let size = iter.len();
313 let mut rules_checked = 0;
314 let mut values_validated = 0;
315
316 if let Some(minsize) = constraints.minsize {
318 rules_checked += 1;
319 if size < minsize {
320 result.is_valid = false;
321 result.errors.push(ValidationError {
322 code: "COLLECTION_TOO_SMALL".to_string(),
323 message: format!("Size {size} is below minimum {minsize}"),
324 field: constraints.fieldname.clone(),
325 suggestion: Some(format!("Provide at least {minsize} elements")),
326 severity: ValidationSeverity::Error,
327 });
328 }
329 }
330
331 if let Some(maxsize) = constraints.maxsize {
332 rules_checked += 1;
333 if size > maxsize {
334 result.is_valid = false;
335 result.errors.push(ValidationError {
336 code: "COLLECTION_TOO_LARGE".to_string(),
337 message: format!("Size {size} exceeds maximum {maxsize}"),
338 field: constraints.fieldname.clone(),
339 suggestion: Some(format!("Limit collection to {maxsize} elements")),
340 severity: ValidationSeverity::Error,
341 });
342 }
343 }
344
345 if let Some(validator) = &constraints.element_validator {
347 for (index, element) in iter.enumerate() {
348 if start_time.elapsed() > self.context.timeout {
350 result.metrics.timed_out = true;
351 result.warnings.push(format!(
352 "Validation timed out after checking {index} elements"
353 ));
354 break;
355 }
356
357 values_validated += 1;
358 rules_checked += 1;
359
360 if let Err(error) = validator.validate(&element) {
361 result.is_valid = false;
362 result.errors.push(ValidationError {
363 code: "ELEMENT_VALIDATION_FAILED".to_string(),
364 message: format!("Index {index}: {error}"),
365 field: constraints.fieldname.clone(),
366 suggestion: Some("Check element constraints".to_string()),
367 severity: ValidationSeverity::Error,
368 });
369 }
370 }
371 }
372
373 result.metrics = ValidationMetrics {
374 duration: start_time.elapsed(),
375 rules_checked,
376 values_validated,
377 memory_used: size * std::mem::size_of::<T>(),
378 timed_out: start_time.elapsed() > self.context.timeout,
379 };
380
381 if let Some(collector) = &mut self.metrics_collector {
383 collector.record_validation(&result);
384 }
385
386 result
387 }
388
389 pub fn validate_string(
391 &mut self,
392 value: &str,
393 constraints: &StringConstraints,
394 ) -> ValidationResult {
395 let start_time = Instant::now();
396 let mut result = ValidationResult {
397 is_valid: true,
398 errors: Vec::new(),
399 warnings: Vec::new(),
400 metrics: ValidationMetrics::default(),
401 };
402
403 let mut rules_checked = 0;
404
405 if let Some(min_length) = constraints.min_length {
407 rules_checked += 1;
408 if value.len() < min_length {
409 result.is_valid = false;
410 result.errors.push(ValidationError {
411 code: "STRING_TOO_SHORT".to_string(),
412 message: format!(
413 "String length {} is less than minimum {}",
414 value.len(),
415 min_length
416 ),
417 field: constraints.fieldname.clone(),
418 suggestion: Some(format!("Provide at least {min_length} characters")),
419 severity: ValidationSeverity::Error,
420 });
421 }
422 }
423
424 if let Some(max_length) = constraints.max_length {
425 rules_checked += 1;
426 if value.len() > max_length {
427 result.is_valid = false;
428 result.errors.push(ValidationError {
429 code: "STRING_TOO_LONG".to_string(),
430 message: format!(
431 "String length {} exceeds maximum {}",
432 value.len(),
433 max_length
434 ),
435 field: constraints.fieldname.clone(),
436 suggestion: Some(format!("Limit string to {max_length} characters")),
437 severity: ValidationSeverity::Error,
438 });
439 }
440 }
441
442 if constraints.check_injection_attacks {
444 rules_checked += 1;
445 if self.contains_injection_patterns(value) {
446 result.is_valid = false;
447 result.errors.push(ValidationError {
448 code: "POTENTIAL_INJECTION_ATTACK".to_string(),
449 message: "String contains potential injection attack patterns".to_string(),
450 field: constraints.fieldname.clone(),
451 suggestion: Some("Sanitize input or use parameterized queries".to_string()),
452 severity: ValidationSeverity::Critical,
453 });
454 }
455 }
456
457 if let Some(allowed_chars) = &constraints.allowed_characters {
459 rules_checked += 1;
460 for ch in value.chars() {
461 if !allowed_chars.contains(&ch) {
462 result.is_valid = false;
463 result.errors.push(ValidationError {
464 code: "INVALID_CHARACTER".to_string(),
465 message: format!("Character '{ch}' is not allowed"),
466 field: constraints.fieldname.clone(),
467 suggestion: Some("Use only allowed characters".to_string()),
468 severity: ValidationSeverity::Error,
469 });
470 break; }
472 }
473 }
474
475 #[cfg(feature = "regex")]
477 if let Some(pattern) = &constraints.pattern {
478 rules_checked += 1;
479 if !pattern.is_match(value) {
480 result.is_valid = false;
481 result.errors.push(ValidationError {
482 code: "PATTERN_MISMATCH".to_string(),
483 message: "String does not match required pattern".to_string(),
484 field: constraints.fieldname.clone(),
485 suggestion: Some("Check string format requirements".to_string()),
486 severity: ValidationSeverity::Error,
487 });
488 }
489 }
490
491 result.metrics = ValidationMetrics {
492 duration: start_time.elapsed(),
493 rules_checked,
494 values_validated: 1,
495 memory_used: value.len(),
496 timed_out: false,
497 };
498
499 if let Some(collector) = &mut self.metrics_collector {
501 collector.record_validation(&result);
502 }
503
504 result
505 }
506
507 fn contains_injection_patterns(&self, value: &str) -> bool {
509 let dangerous_patterns = [
510 "'; DROP TABLE",
512 "' OR '1'='1",
513 "UNION SELECT",
514 "'; --",
515 "<script",
517 "javascript:",
518 "onload=",
519 "onerror=",
520 "; rm -rf",
522 "| rm -rf",
523 "&& rm -rf",
524 "$(rm -rf)",
525 "../",
527 "..\\",
528 "%2e%2e%2f",
529 "%2e%2e\\",
530 ];
531
532 let value_lower = value.to_lowercase();
533 dangerous_patterns
534 .iter()
535 .any(|pattern| value_lower.contains(&pattern.to_lowercase()))
536 }
537
538 pub fn clear_cache(&mut self) {
540 self.cache.clear();
541 }
542
543 pub fn get_metrics(&self) -> Option<ValidationSummary> {
545 self.metrics_collector
546 .as_ref()
547 .map(|collector| collector.get_metrics())
548 }
549}
550
551impl Default for ProductionValidator {
552 fn default() -> Self {
553 Self::new()
554 }
555}
556
557#[derive(Debug)]
559pub struct NumericConstraints<T> {
560 pub min: Option<T>,
562 pub max: Option<T>,
564 pub fieldname: Option<String>,
566 pub custom_rules: Vec<Box<dyn NumericRule<T>>>,
568 pub allow_custom_rule_failures: bool,
570}
571
572impl<T: Clone> Clone for NumericConstraints<T> {
573 fn clone(&self) -> Self {
574 Self {
575 min: self.min.clone(),
576 max: self.max.clone(),
577 fieldname: self.fieldname.clone(),
578 custom_rules: Vec::new(), allow_custom_rule_failures: self.allow_custom_rule_failures,
580 }
581 }
582}
583
584pub trait NumericRule<T>: std::fmt::Debug {
586 fn validate(&self, value: T) -> Result<(), String>;
588}
589
590#[derive(Debug)]
592pub struct CollectionConstraints<T> {
593 pub minsize: Option<usize>,
595 pub maxsize: Option<usize>,
597 pub element_validator: Option<Box<dyn ElementValidator<T>>>,
599 pub fieldname: Option<String>,
601}
602
603pub trait ElementValidator<T>: std::fmt::Debug {
605 fn validate(&self, element: &T) -> Result<(), String>;
607}
608
609#[derive(Debug)]
611pub struct StringConstraints {
612 pub min_length: Option<usize>,
614 pub max_length: Option<usize>,
616 pub allowed_characters: Option<std::collections::HashSet<char>>,
618 #[cfg(feature = "regex")]
620 pub pattern: Option<regex::Regex>,
621 pub check_injection_attacks: bool,
623 pub fieldname: Option<String>,
625}
626
627pub struct DefaultMetricsCollector {
629 results: Vec<ValidationResult>,
631}
632
633impl DefaultMetricsCollector {
634 pub fn new() -> Self {
636 Self {
637 results: Vec::new(),
638 }
639 }
640}
641
642impl ValidationMetricsCollector for DefaultMetricsCollector {
643 fn record_validation(&mut self, result: &ValidationResult) {
644 self.results.push(result.clone());
645 }
646
647 fn get_metrics(&self) -> ValidationSummary {
648 let total_validations = self.results.len();
649
650 if total_validations == 0 {
651 return ValidationSummary {
652 total_validations: 0,
653 total_duration: Duration::ZERO,
654 average_duration: Duration::ZERO,
655 success_rate: 0.0,
656 commonerrors: HashMap::new(),
657 };
658 }
659
660 let total_duration: Duration = self.results.iter().map(|r| r.metrics.duration).sum();
661
662 let average_duration = total_duration / total_validations as u32;
663
664 let successful_validations = self.results.iter().filter(|r| r.is_valid).count();
665
666 let success_rate = successful_validations as f64 / total_validations as f64;
667
668 let mut commonerrors = HashMap::new();
669 for result in &self.results {
670 for error in &result.errors {
671 *commonerrors.entry(error.code.clone()).or_insert(0) += 1;
672 }
673 }
674
675 ValidationSummary {
676 total_validations,
677 total_duration,
678 average_duration,
679 success_rate,
680 commonerrors,
681 }
682 }
683
684 fn clear(&mut self) {
685 self.results.clear();
686 }
687}
688
689impl Default for DefaultMetricsCollector {
690 fn default() -> Self {
691 Self::new()
692 }
693}
694
695#[allow(dead_code)]
698pub fn validate_positive<T>(value: T, fieldname: Option<String>) -> CoreResult<T>
699where
700 T: PartialOrd + Copy + fmt::Debug + fmt::Display + Default,
701{
702 let mut validator = ProductionValidator::new();
703 let constraints = NumericConstraints {
704 min: Some(T::default()), max: None,
706 fieldname,
707 custom_rules: Vec::new(),
708 allow_custom_rule_failures: false,
709 };
710
711 let result = validator.validate_numeric(value, &constraints);
712 if result.is_valid {
713 Ok(value)
714 } else {
715 Err(CoreError::ValidationError(ErrorContext::new(format!(
716 "Validation failed: {:?}",
717 result.errors
718 ))))
719 }
720}
721
722#[allow(dead_code)]
724pub fn validate_dimensions(dims: &[usize]) -> CoreResult<()> {
725 if dims.is_empty() {
726 return Err(CoreError::ValidationError(ErrorContext::new(
727 "Array must have at least one dimension",
728 )));
729 }
730
731 if dims.len() > MAX_DIMENSIONS {
732 return Err(CoreError::ValidationError(ErrorContext::new(format!(
733 "Array has {} dimensions, maximum allowed is {}",
734 dims.len(),
735 MAX_DIMENSIONS
736 ))));
737 }
738
739 for (i, &dim) in dims.iter().enumerate() {
740 if dim == 0 {
741 return Err(CoreError::ValidationError(ErrorContext::new(format!(
742 "Dimension {i} has size 0, which is not allowed"
743 ))));
744 }
745
746 if dim > usize::MAX / 1024 {
748 return Err(CoreError::ValidationError(ErrorContext::new(format!(
749 "Dimension {i} size {dim} is too large"
750 ))));
751 }
752 }
753
754 let total_size = dims.iter().try_fold(1usize, |acc, &dim| {
756 acc.checked_mul(dim).ok_or_else(|| {
757 CoreError::ValidationError(ErrorContext::new("Array total size would overflow"))
758 })
759 })?;
760
761 const MAX_TOTAL_SIZE: usize = 1024 * 1024 * 1024 / 8;
763 if total_size > MAX_TOTAL_SIZE {
764 return Err(CoreError::ValidationError(ErrorContext::new(format!(
765 "Array total size {total_size} exceeds maximum allowed size {MAX_TOTAL_SIZE}"
766 ))));
767 }
768
769 Ok(())
770}
771
772#[allow(dead_code)]
774pub fn validate_file_path(path: &str) -> CoreResult<()> {
775 if path.contains("..") {
777 return Err(CoreError::ValidationError(ErrorContext::new(
778 "Path traversal detected in file path",
779 )));
780 }
781
782 if path.contains('\0') {
784 return Err(CoreError::ValidationError(ErrorContext::new(
785 "Null byte detected in file path",
786 )));
787 }
788
789 if path.len() > 4096 {
791 return Err(CoreError::ValidationError(ErrorContext::new(
792 "File path too long",
793 )));
794 }
795
796 let dangerous_patterns = ["/dev/", "/proc/", "/sys/", "//", "\\\\"];
798 for pattern in &dangerous_patterns {
799 if path.contains(pattern) {
800 return Err(CoreError::ValidationError(ErrorContext::new(format!(
801 "Dangerous pattern '{pattern}' detected in file path"
802 ))));
803 }
804 }
805
806 Ok(())
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use std::collections::HashSet;
813
814 #[test]
815 fn test_numeric_validation() {
816 let mut validator = ProductionValidator::new();
817 let constraints = NumericConstraints {
818 min: Some(0.0),
819 max: Some(100.0),
820 fieldname: Some("test_value".to_string()),
821 custom_rules: Vec::new(),
822 allow_custom_rule_failures: false,
823 };
824
825 let result = validator.validate_numeric(50.0, &constraints);
827 assert!(result.is_valid);
828 assert!(result.errors.is_empty());
829
830 let result = validator.validate_numeric(-10.0, &constraints);
832 assert!(!result.is_valid);
833 assert!(result.errors.iter().any(|e| e.code == "VALUE_TOO_SMALL"));
834
835 let result = validator.validate_numeric(150.0, &constraints);
837 assert!(!result.is_valid);
838 assert!(result.errors.iter().any(|e| e.code == "VALUE_TOO_LARGE"));
839 }
840
841 #[test]
842 fn test_string_validation() {
843 let mut validator = ProductionValidator::new();
844 let mut allowed_chars = HashSet::new();
845 allowed_chars.extend("abcdefghijklmnopqrstuvwxyz0123456789".chars());
846
847 let constraints = StringConstraints {
848 min_length: Some(3),
849 max_length: Some(20),
850 allowed_characters: Some(allowed_chars),
851 #[cfg(feature = "regex")]
852 pattern: None,
853 check_injection_attacks: true,
854 fieldname: Some("username".to_string()),
855 };
856
857 let result = validator.validate_string("validuser123", &constraints);
859 assert!(result.is_valid);
860
861 let result = validator.validate_string("ab", &constraints);
863 assert!(!result.is_valid);
864
865 let result = validator.validate_string("user@name", &constraints);
867 assert!(!result.is_valid);
868
869 let result = validator.validate_string("'; DROP TABLE users; --", &constraints);
871 assert!(!result.is_valid);
872 assert!(result
873 .errors
874 .iter()
875 .any(|e| e.code == "POTENTIAL_INJECTION_ATTACK"));
876 }
877
878 #[test]
879 fn test_array_dimensions_validation() {
880 assert!(validate_dimensions(&[10, 20, 30]).is_ok());
882
883 assert!(validate_dimensions(&[]).is_err());
885
886 assert!(validate_dimensions(&[10, 0, 30]).is_err());
888
889 assert!(validate_dimensions(&[1, 2, 3, 4, 5, 6]).is_err());
891 }
892
893 #[test]
894 fn test_file_path_validation() {
895 assert!(validate_file_path("/home/user/data.txt").is_ok());
897
898 assert!(validate_file_path("../../../etc/passwd").is_err());
900
901 assert!(validate_file_path("/home/user\0/data.txt").is_err());
903
904 assert!(validate_file_path("/dev/null").is_err());
906 }
907
908 #[test]
909 fn test_metrics_collection() {
910 let mut validator = ProductionValidator::new()
911 .with_metrics_collector(Box::new(DefaultMetricsCollector::new()));
912
913 let constraints = NumericConstraints {
914 min: Some(0),
915 max: Some(100),
916 fieldname: None,
917 custom_rules: Vec::new(),
918 allow_custom_rule_failures: false,
919 };
920
921 validator.validate_numeric(50, &constraints);
923 validator.validate_numeric(150, &constraints); validator.validate_numeric(25, &constraints);
925
926 let metrics = validator.get_metrics().expect("Operation failed");
927 assert_eq!(metrics.total_validations, 3);
928 assert_eq!(metrics.success_rate, 2.0 / 3.0);
929 assert!(metrics.commonerrors.contains_key("VALUE_TOO_LARGE"));
930 }
931}