1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ValidationResult {
10 Valid,
12 InvalidSize,
14 InvalidHash,
16 InvalidFormat,
18 Corrupted,
20}
21
22#[derive(Debug, Clone)]
24pub struct ValidationRule {
25 pub name: String,
27 pub max_size: Option<u64>,
29 pub min_size: Option<u64>,
31 pub required_prefix: Option<Vec<u8>>,
33 pub expected_hash: Option<u64>,
35}
36
37#[derive(Debug, Clone)]
39pub struct ValidationReport {
40 pub block_cid: String,
42 pub result: ValidationResult,
44 pub checks_passed: usize,
46 pub checks_failed: usize,
48 pub details: Vec<String>,
50}
51
52#[derive(Debug, Clone)]
54pub struct ValidatorStats {
55 pub rules_count: usize,
57 pub blocks_validated: u64,
59 pub blocks_valid: u64,
61 pub blocks_invalid: u64,
63 pub validity_ratio: f64,
65}
66
67pub struct StorageBlockValidator {
72 rules: Vec<ValidationRule>,
73 blocks_validated: u64,
74 blocks_valid: u64,
75 blocks_invalid: u64,
76}
77
78impl StorageBlockValidator {
79 pub fn new() -> Self {
81 Self {
82 rules: Vec::new(),
83 blocks_validated: 0,
84 blocks_valid: 0,
85 blocks_invalid: 0,
86 }
87 }
88
89 pub fn add_rule(&mut self, rule: ValidationRule) {
91 self.rules.push(rule);
92 }
93
94 pub fn remove_rule(&mut self, name: &str) -> bool {
96 let before = self.rules.len();
97 self.rules.retain(|r| r.name != name);
98 self.rules.len() < before
99 }
100
101 pub fn rule_count(&self) -> usize {
103 self.rules.len()
104 }
105
106 pub fn validate(&mut self, block_cid: &str, data: &[u8]) -> ValidationReport {
112 let mut checks_passed: usize = 0;
113 let mut checks_failed: usize = 0;
114 let mut details: Vec<String> = Vec::new();
115 let mut last_failure: Option<ValidationResult> = None;
116
117 if self.rules.is_empty() {
118 self.blocks_validated += 1;
120 self.blocks_valid += 1;
121 return ValidationReport {
122 block_cid: block_cid.to_string(),
123 result: ValidationResult::Valid,
124 checks_passed: 0,
125 checks_failed: 0,
126 details: vec!["no rules configured; block accepted".to_string()],
127 };
128 }
129
130 for rule in &self.rules {
131 if rule.min_size.is_some() || rule.max_size.is_some() {
133 if Self::validate_size(data, rule.min_size, rule.max_size) {
134 checks_passed += 1;
135 details.push(format!("[{}] size check passed", rule.name));
136 } else {
137 checks_failed += 1;
138 details.push(format!(
139 "[{}] size check failed: len={} min={:?} max={:?}",
140 rule.name,
141 data.len(),
142 rule.min_size,
143 rule.max_size
144 ));
145 last_failure = Some(ValidationResult::InvalidSize);
146 }
147 }
148
149 if let Some(ref prefix) = rule.required_prefix {
151 if Self::validate_prefix(data, prefix) {
152 checks_passed += 1;
153 details.push(format!("[{}] prefix check passed", rule.name));
154 } else {
155 checks_failed += 1;
156 details.push(format!("[{}] prefix check failed", rule.name));
157 last_failure = Some(ValidationResult::InvalidFormat);
158 }
159 }
160
161 if let Some(expected) = rule.expected_hash {
163 if Self::validate_hash(data, expected) {
164 checks_passed += 1;
165 details.push(format!("[{}] hash check passed", rule.name));
166 } else {
167 checks_failed += 1;
168 let actual = Self::fnv1a_hash(data);
169 details.push(format!(
170 "[{}] hash check failed: expected={} actual={}",
171 rule.name, expected, actual
172 ));
173 last_failure = Some(ValidationResult::InvalidHash);
174 }
175 }
176 }
177
178 let result = if checks_failed == 0 {
179 ValidationResult::Valid
180 } else if checks_failed > 1 {
181 ValidationResult::Corrupted
182 } else {
183 last_failure.unwrap_or(ValidationResult::Corrupted)
185 };
186
187 self.blocks_validated += 1;
188 if result == ValidationResult::Valid {
189 self.blocks_valid += 1;
190 } else {
191 self.blocks_invalid += 1;
192 }
193
194 ValidationReport {
195 block_cid: block_cid.to_string(),
196 result,
197 checks_passed,
198 checks_failed,
199 details,
200 }
201 }
202
203 pub fn validate_size(data: &[u8], min: Option<u64>, max: Option<u64>) -> bool {
205 let len = data.len() as u64;
206 if let Some(lo) = min {
207 if len < lo {
208 return false;
209 }
210 }
211 if let Some(hi) = max {
212 if len > hi {
213 return false;
214 }
215 }
216 true
217 }
218
219 pub fn validate_prefix(data: &[u8], prefix: &[u8]) -> bool {
221 data.starts_with(prefix)
222 }
223
224 pub fn validate_hash(data: &[u8], expected: u64) -> bool {
226 Self::fnv1a_hash(data) == expected
227 }
228
229 pub fn fnv1a_hash(data: &[u8]) -> u64 {
231 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
232 const FNV_PRIME: u64 = 0x0100_0000_01b3;
233 let mut hash = FNV_OFFSET;
234 for &byte in data {
235 hash ^= byte as u64;
236 hash = hash.wrapping_mul(FNV_PRIME);
237 }
238 hash
239 }
240
241 pub fn batch_validate(&mut self, blocks: &[(&str, &[u8])]) -> Vec<ValidationReport> {
243 blocks
244 .iter()
245 .map(|(cid, data)| self.validate(cid, data))
246 .collect()
247 }
248
249 pub fn stats(&self) -> ValidatorStats {
251 let validity_ratio = if self.blocks_validated == 0 {
252 0.0
253 } else {
254 self.blocks_valid as f64 / self.blocks_validated as f64
255 };
256 ValidatorStats {
257 rules_count: self.rules.len(),
258 blocks_validated: self.blocks_validated,
259 blocks_valid: self.blocks_valid,
260 blocks_invalid: self.blocks_invalid,
261 validity_ratio,
262 }
263 }
264}
265
266impl Default for StorageBlockValidator {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272#[cfg(test)]
277mod tests {
278 use super::*;
279
280 fn rule(name: &str) -> ValidationRule {
282 ValidationRule {
283 name: name.to_string(),
284 max_size: None,
285 min_size: None,
286 required_prefix: None,
287 expected_hash: None,
288 }
289 }
290
291 #[test]
294 fn test_fnv1a_empty() {
295 assert_eq!(
297 StorageBlockValidator::fnv1a_hash(b""),
298 0xcbf2_9ce4_8422_2325
299 );
300 }
301
302 #[test]
303 fn test_fnv1a_known_vector() {
304 let hash = StorageBlockValidator::fnv1a_hash(b"hello");
306 assert_eq!(hash, 0xa430_d846_80aa_bd0b);
307 }
308
309 #[test]
310 fn test_fnv1a_different_inputs_differ() {
311 let h1 = StorageBlockValidator::fnv1a_hash(b"abc");
312 let h2 = StorageBlockValidator::fnv1a_hash(b"abd");
313 assert_ne!(h1, h2);
314 }
315
316 #[test]
317 fn test_fnv1a_deterministic() {
318 let data = b"some block data for hashing";
319 let h1 = StorageBlockValidator::fnv1a_hash(data);
320 let h2 = StorageBlockValidator::fnv1a_hash(data);
321 assert_eq!(h1, h2);
322 }
323
324 #[test]
327 fn test_validate_size_within_bounds() {
328 assert!(StorageBlockValidator::validate_size(
329 &[0u8; 100],
330 Some(50),
331 Some(200)
332 ));
333 }
334
335 #[test]
336 fn test_validate_size_too_small() {
337 assert!(!StorageBlockValidator::validate_size(
338 &[0u8; 10],
339 Some(50),
340 None
341 ));
342 }
343
344 #[test]
345 fn test_validate_size_too_large() {
346 assert!(!StorageBlockValidator::validate_size(
347 &[0u8; 300],
348 None,
349 Some(200)
350 ));
351 }
352
353 #[test]
354 fn test_validate_size_no_bounds() {
355 assert!(StorageBlockValidator::validate_size(
356 &[0u8; 999],
357 None,
358 None
359 ));
360 }
361
362 #[test]
363 fn test_validate_size_exact_min() {
364 assert!(StorageBlockValidator::validate_size(
365 &[0u8; 50],
366 Some(50),
367 None
368 ));
369 }
370
371 #[test]
372 fn test_validate_size_exact_max() {
373 assert!(StorageBlockValidator::validate_size(
374 &[0u8; 200],
375 None,
376 Some(200)
377 ));
378 }
379
380 #[test]
383 fn test_validate_prefix_match() {
384 assert!(StorageBlockValidator::validate_prefix(
385 &[0x89, 0x50, 0x4E, 0x47, 0xFF],
386 &[0x89, 0x50, 0x4E, 0x47]
387 ));
388 }
389
390 #[test]
391 fn test_validate_prefix_mismatch() {
392 assert!(!StorageBlockValidator::validate_prefix(
393 &[0x00, 0x01, 0x02],
394 &[0x89, 0x50]
395 ));
396 }
397
398 #[test]
399 fn test_validate_prefix_data_shorter_than_prefix() {
400 assert!(!StorageBlockValidator::validate_prefix(
401 &[0x89],
402 &[0x89, 0x50]
403 ));
404 }
405
406 #[test]
407 fn test_validate_prefix_empty_prefix() {
408 assert!(StorageBlockValidator::validate_prefix(b"anything", b""));
409 }
410
411 #[test]
414 fn test_validate_hash_correct() {
415 let data = b"test block";
416 let expected = StorageBlockValidator::fnv1a_hash(data);
417 assert!(StorageBlockValidator::validate_hash(data, expected));
418 }
419
420 #[test]
421 fn test_validate_hash_wrong() {
422 assert!(!StorageBlockValidator::validate_hash(b"data", 0));
423 }
424
425 #[test]
428 fn test_validate_valid_block() {
429 let data = b"valid block content";
430 let hash = StorageBlockValidator::fnv1a_hash(data);
431 let mut v = StorageBlockValidator::new();
432 v.add_rule(ValidationRule {
433 name: "basic".to_string(),
434 max_size: Some(1024),
435 min_size: Some(1),
436 required_prefix: Some(b"valid".to_vec()),
437 expected_hash: Some(hash),
438 });
439 let report = v.validate("Qm123", data);
440 assert_eq!(report.result, ValidationResult::Valid);
441 assert_eq!(report.checks_failed, 0);
442 assert!(report.checks_passed >= 3);
443 assert_eq!(report.block_cid, "Qm123");
444 }
445
446 #[test]
447 fn test_validate_size_violation() {
448 let mut v = StorageBlockValidator::new();
449 v.add_rule(ValidationRule {
450 name: "size-only".to_string(),
451 max_size: Some(5),
452 min_size: None,
453 required_prefix: None,
454 expected_hash: None,
455 });
456 let report = v.validate("cid1", b"too long data here");
457 assert_eq!(report.result, ValidationResult::InvalidSize);
458 assert_eq!(report.checks_failed, 1);
459 }
460
461 #[test]
462 fn test_validate_hash_mismatch() {
463 let mut v = StorageBlockValidator::new();
464 v.add_rule(ValidationRule {
465 name: "hash-check".to_string(),
466 max_size: None,
467 min_size: None,
468 required_prefix: None,
469 expected_hash: Some(12345),
470 });
471 let report = v.validate("cid2", b"some data");
472 assert_eq!(report.result, ValidationResult::InvalidHash);
473 }
474
475 #[test]
476 fn test_validate_prefix_failure() {
477 let mut v = StorageBlockValidator::new();
478 v.add_rule(ValidationRule {
479 name: "magic".to_string(),
480 max_size: None,
481 min_size: None,
482 required_prefix: Some(vec![0xFF, 0xFE]),
483 expected_hash: None,
484 });
485 let report = v.validate("cid3", b"\x00\x01rest");
486 assert_eq!(report.result, ValidationResult::InvalidFormat);
487 }
488
489 #[test]
490 fn test_validate_corrupted_multiple_failures() {
491 let mut v = StorageBlockValidator::new();
492 v.add_rule(ValidationRule {
493 name: "strict".to_string(),
494 max_size: Some(5),
495 min_size: None,
496 required_prefix: Some(vec![0xAA]),
497 expected_hash: Some(0),
498 });
499 let report = v.validate("cid4", b"this is clearly wrong data");
501 assert_eq!(report.result, ValidationResult::Corrupted);
502 assert!(report.checks_failed > 1);
503 }
504
505 #[test]
506 fn test_validate_no_rules_all_valid() {
507 let mut v = StorageBlockValidator::new();
508 let report = v.validate("cid5", b"anything");
509 assert_eq!(report.result, ValidationResult::Valid);
510 }
511
512 #[test]
513 fn test_validate_empty_data() {
514 let mut v = StorageBlockValidator::new();
515 v.add_rule(ValidationRule {
516 name: "needs-content".to_string(),
517 max_size: None,
518 min_size: Some(1),
519 required_prefix: None,
520 expected_hash: None,
521 });
522 let report = v.validate("cid6", b"");
523 assert_eq!(report.result, ValidationResult::InvalidSize);
524 }
525
526 #[test]
527 fn test_validate_empty_data_no_rules() {
528 let mut v = StorageBlockValidator::new();
529 let report = v.validate("cid7", b"");
530 assert_eq!(report.result, ValidationResult::Valid);
531 }
532
533 #[test]
534 fn test_validate_multiple_rules_and_logic() {
535 let data = b"\x89PNG rest of image";
536 let hash = StorageBlockValidator::fnv1a_hash(data);
537 let mut v = StorageBlockValidator::new();
538 v.add_rule(ValidationRule {
540 name: "size".to_string(),
541 max_size: Some(1024),
542 min_size: Some(1),
543 required_prefix: None,
544 expected_hash: None,
545 });
546 v.add_rule(ValidationRule {
548 name: "format".to_string(),
549 max_size: None,
550 min_size: None,
551 required_prefix: Some(b"\x89PNG".to_vec()),
552 expected_hash: None,
553 });
554 v.add_rule(ValidationRule {
556 name: "integrity".to_string(),
557 max_size: None,
558 min_size: None,
559 required_prefix: None,
560 expected_hash: Some(hash),
561 });
562 let report = v.validate("img-cid", data);
563 assert_eq!(report.result, ValidationResult::Valid);
564 assert_eq!(report.checks_passed, 3);
565 assert_eq!(report.checks_failed, 0);
566 }
567
568 #[test]
571 fn test_batch_validate() {
572 let mut v = StorageBlockValidator::new();
573 v.add_rule(ValidationRule {
574 name: "min-size".to_string(),
575 max_size: None,
576 min_size: Some(3),
577 required_prefix: None,
578 expected_hash: None,
579 });
580 let blocks: Vec<(&str, &[u8])> =
581 vec![("ok1", b"abcdef"), ("ok2", b"xyz"), ("fail1", b"ab")];
582 let reports = v.batch_validate(&blocks);
583 assert_eq!(reports.len(), 3);
584 assert_eq!(reports[0].result, ValidationResult::Valid);
585 assert_eq!(reports[1].result, ValidationResult::Valid);
586 assert_eq!(reports[2].result, ValidationResult::InvalidSize);
587 }
588
589 #[test]
590 fn test_batch_validate_empty() {
591 let mut v = StorageBlockValidator::new();
592 let reports = v.batch_validate(&[]);
593 assert!(reports.is_empty());
594 }
595
596 #[test]
599 fn test_remove_rule_exists() {
600 let mut v = StorageBlockValidator::new();
601 v.add_rule(rule("alpha"));
602 v.add_rule(rule("beta"));
603 assert!(v.remove_rule("alpha"));
604 assert_eq!(v.rule_count(), 1);
605 }
606
607 #[test]
608 fn test_remove_rule_not_found() {
609 let mut v = StorageBlockValidator::new();
610 v.add_rule(rule("alpha"));
611 assert!(!v.remove_rule("gamma"));
612 assert_eq!(v.rule_count(), 1);
613 }
614
615 #[test]
618 fn test_stats_initial() {
619 let v = StorageBlockValidator::new();
620 let s = v.stats();
621 assert_eq!(s.blocks_validated, 0);
622 assert_eq!(s.blocks_valid, 0);
623 assert_eq!(s.blocks_invalid, 0);
624 assert!((s.validity_ratio - 0.0).abs() < f64::EPSILON);
625 }
626
627 #[test]
628 fn test_stats_after_validations() {
629 let mut v = StorageBlockValidator::new();
630 v.add_rule(ValidationRule {
631 name: "size".to_string(),
632 max_size: Some(10),
633 min_size: None,
634 required_prefix: None,
635 expected_hash: None,
636 });
637 let _ = v.validate("a", b"short");
638 let _ = v.validate("b", b"this is way too long for the rule");
639 let _ = v.validate("c", b"ok");
640 let s = v.stats();
641 assert_eq!(s.blocks_validated, 3);
642 assert_eq!(s.blocks_valid, 2);
643 assert_eq!(s.blocks_invalid, 1);
644 assert!((s.validity_ratio - 2.0 / 3.0).abs() < 1e-9);
645 assert_eq!(s.rules_count, 1);
646 }
647
648 #[test]
649 fn test_stats_all_valid() {
650 let mut v = StorageBlockValidator::new();
651 let _ = v.validate("x", b"data");
653 let s = v.stats();
654 assert!((s.validity_ratio - 1.0).abs() < f64::EPSILON);
655 }
656
657 #[test]
660 fn test_rule_count() {
661 let mut v = StorageBlockValidator::new();
662 assert_eq!(v.rule_count(), 0);
663 v.add_rule(rule("a"));
664 v.add_rule(rule("b"));
665 assert_eq!(v.rule_count(), 2);
666 v.remove_rule("a");
667 assert_eq!(v.rule_count(), 1);
668 }
669
670 #[test]
673 fn test_default() {
674 let v = StorageBlockValidator::default();
675 assert_eq!(v.rule_count(), 0);
676 assert_eq!(v.stats().blocks_validated, 0);
677 }
678
679 #[test]
682 fn test_details_contain_rule_names() {
683 let data = b"hello";
684 let hash = StorageBlockValidator::fnv1a_hash(data);
685 let mut v = StorageBlockValidator::new();
686 v.add_rule(ValidationRule {
687 name: "my-rule".to_string(),
688 max_size: Some(1024),
689 min_size: None,
690 required_prefix: None,
691 expected_hash: Some(hash),
692 });
693 let report = v.validate("cid", data);
694 assert!(report.details.iter().all(|d| d.contains("my-rule")));
695 }
696
697 #[test]
700 fn test_rule_with_no_checks_passes() {
701 let mut v = StorageBlockValidator::new();
702 v.add_rule(rule("empty-rule"));
704 let report = v.validate("cid", b"data");
705 assert_eq!(report.result, ValidationResult::Valid);
707 assert_eq!(report.checks_passed, 0);
708 assert_eq!(report.checks_failed, 0);
709 }
710}