Skip to main content

ipfrs_storage/
block_validator.rs

1//! Storage block validator with multiple hash and integrity checks.
2//!
3//! Provides [`StorageBlockValidator`] for validating block integrity
4//! using configurable rules including size bounds, magic byte prefixes,
5//! and FNV-1a hash verification.
6
7/// Result of a block validation pass.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ValidationResult {
10    /// Block passed all validation rules.
11    Valid,
12    /// Block size fell outside the allowed range.
13    InvalidSize,
14    /// Block hash did not match the expected value.
15    InvalidHash,
16    /// Block format (e.g. magic bytes) was incorrect.
17    InvalidFormat,
18    /// Block appears corrupted (multiple failures).
19    Corrupted,
20}
21
22/// A single validation rule applied to a block.
23#[derive(Debug, Clone)]
24pub struct ValidationRule {
25    /// Human-readable name for this rule.
26    pub name: String,
27    /// Maximum allowed block size in bytes.
28    pub max_size: Option<u64>,
29    /// Minimum allowed block size in bytes.
30    pub min_size: Option<u64>,
31    /// Required prefix (magic bytes) that the block data must start with.
32    pub required_prefix: Option<Vec<u8>>,
33    /// Expected FNV-1a 64-bit hash of the block data.
34    pub expected_hash: Option<u64>,
35}
36
37/// Detailed report produced after validating a single block.
38#[derive(Debug, Clone)]
39pub struct ValidationReport {
40    /// CID (content identifier) of the validated block.
41    pub block_cid: String,
42    /// Overall validation result.
43    pub result: ValidationResult,
44    /// Number of individual checks that passed.
45    pub checks_passed: usize,
46    /// Number of individual checks that failed.
47    pub checks_failed: usize,
48    /// Human-readable details for each check outcome.
49    pub details: Vec<String>,
50}
51
52/// Aggregate statistics for the validator.
53#[derive(Debug, Clone)]
54pub struct ValidatorStats {
55    /// Number of registered validation rules.
56    pub rules_count: usize,
57    /// Total number of blocks validated.
58    pub blocks_validated: u64,
59    /// Number of blocks that passed validation.
60    pub blocks_valid: u64,
61    /// Number of blocks that failed validation.
62    pub blocks_invalid: u64,
63    /// Ratio of valid blocks to total validated (0.0–1.0).
64    pub validity_ratio: f64,
65}
66
67/// Block integrity validator with configurable rules.
68///
69/// Validates blocks against a set of [`ValidationRule`]s, tracking
70/// cumulative statistics across invocations.
71pub struct StorageBlockValidator {
72    rules: Vec<ValidationRule>,
73    blocks_validated: u64,
74    blocks_valid: u64,
75    blocks_invalid: u64,
76}
77
78impl StorageBlockValidator {
79    /// Create a new validator with no rules.
80    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    /// Register a validation rule.
90    pub fn add_rule(&mut self, rule: ValidationRule) {
91        self.rules.push(rule);
92    }
93
94    /// Remove a rule by name. Returns `true` if a rule was removed.
95    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    /// Number of registered rules.
102    pub fn rule_count(&self) -> usize {
103        self.rules.len()
104    }
105
106    /// Validate a single block against all registered rules.
107    ///
108    /// Every rule is evaluated; a block is considered valid only if it
109    /// passes **all** checks across **all** rules.  When more than one
110    /// check fails the result is [`ValidationResult::Corrupted`].
111    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            // No rules means the block is trivially valid.
119            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            // Size check
132            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            // Prefix check
150            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            // Hash check
162            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            // Exactly one failure – use the specific failure kind.
184            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    /// Check whether `data` length is within `[min, max]`.
204    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    /// Check whether `data` starts with the given `prefix`.
220    pub fn validate_prefix(data: &[u8], prefix: &[u8]) -> bool {
221        data.starts_with(prefix)
222    }
223
224    /// Check whether the FNV-1a hash of `data` equals `expected`.
225    pub fn validate_hash(data: &[u8], expected: u64) -> bool {
226        Self::fnv1a_hash(data) == expected
227    }
228
229    /// Compute the FNV-1a 64-bit hash of `data`.
230    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    /// Validate multiple blocks, returning a report for each.
242    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    /// Return aggregate validation statistics.
250    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// ---------------------------------------------------------------------------
273// Tests
274// ---------------------------------------------------------------------------
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    // Helper: build a simple rule with only a name.
281    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    // ----- FNV-1a correctness -----
292
293    #[test]
294    fn test_fnv1a_empty() {
295        // FNV-1a 64-bit of empty input is the offset basis.
296        assert_eq!(
297            StorageBlockValidator::fnv1a_hash(b""),
298            0xcbf2_9ce4_8422_2325
299        );
300    }
301
302    #[test]
303    fn test_fnv1a_known_vector() {
304        // "hello" – verify deterministic output.
305        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    // ----- validate_size -----
325
326    #[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    // ----- validate_prefix -----
381
382    #[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    // ----- validate_hash -----
412
413    #[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    // ----- validate (full pipeline) -----
426
427    #[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        // Data fails size, prefix, and hash.
500        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        // Rule 1: size
539        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        // Rule 2: format
547        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        // Rule 3: integrity
555        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    // ----- batch_validate -----
569
570    #[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    // ----- remove_rule -----
597
598    #[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    // ----- stats -----
616
617    #[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        // No rules => everything valid.
652        let _ = v.validate("x", b"data");
653        let s = v.stats();
654        assert!((s.validity_ratio - 1.0).abs() < f64::EPSILON);
655    }
656
657    // ----- rule_count -----
658
659    #[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    // ----- Default impl -----
671
672    #[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    // ----- details content -----
680
681    #[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    // ----- edge: rule with no checks -----
698
699    #[test]
700    fn test_rule_with_no_checks_passes() {
701        let mut v = StorageBlockValidator::new();
702        // Rule exists but defines no actual checks.
703        v.add_rule(rule("empty-rule"));
704        let report = v.validate("cid", b"data");
705        // No checks run so nothing fails.
706        assert_eq!(report.result, ValidationResult::Valid);
707        assert_eq!(report.checks_passed, 0);
708        assert_eq!(report.checks_failed, 0);
709    }
710}