1use std::collections::HashMap;
9use std::time::{SystemTime, UNIX_EPOCH};
10use thiserror::Error;
11
12#[inline]
18fn fnv1a_64(data: &[u8]) -> u64 {
19 const OFFSET_BASIS: u64 = 14695981039346656037;
20 const PRIME: u64 = 1099511628211;
21 let mut hash = OFFSET_BASIS;
22 for &byte in data {
23 hash ^= byte as u64;
24 hash = hash.wrapping_mul(PRIME);
25 }
26 hash
27}
28
29#[inline]
31fn adler32(data: &[u8]) -> u32 {
32 let (mut a, mut b) = (1u32, 0u32);
33 for &byte in data {
34 a = a.wrapping_add(byte as u32) % 65521;
35 b = b.wrapping_add(a) % 65521;
36 }
37 (b << 16) | a
38}
39
40#[inline]
42fn crc16(data: &[u8]) -> u16 {
43 let mut crc = 0xFFFFu16;
44 for &byte in data {
45 let mut x = (crc >> 8) ^ (byte as u16);
46 x ^= x >> 4;
47 crc = (crc << 8) ^ (x << 12) ^ (x << 5) ^ x;
48 }
49 crc
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct IntegrityHash {
59 pub fnv1a: u64,
61 pub adler32: u32,
63 pub crc16: u16,
65 pub size_bytes: usize,
67}
68
69impl IntegrityHash {
70 fn compute(data: &[u8]) -> Self {
71 Self {
72 fnv1a: fnv1a_64(data),
73 adler32: adler32(data),
74 crc16: crc16(data),
75 size_bytes: data.len(),
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum IntegrityLevel {
83 Quick,
85 Standard,
87 Full,
89 Custom(Vec<String>),
91}
92
93impl IntegrityLevel {
94 pub const SUPPORTED: &'static [&'static str] = &["fnv1a", "adler32", "crc16"];
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum OicIntegrityStatus {
101 Unknown,
103 Verified(u64),
105 Corrupted {
107 detected_at: u64,
109 expected: IntegrityHash,
111 got: IntegrityHash,
113 },
114 PossiblyCorrupted(String),
116}
117
118#[derive(Debug, Clone)]
120pub struct OicObjectRecord {
121 pub id: String,
123 pub hash: IntegrityHash,
125 pub created_at: u64,
127 pub last_verified_at: u64,
129 pub verification_count: u64,
131 pub status: OicIntegrityStatus,
133}
134
135#[derive(Debug, Clone)]
137pub struct OicVerificationResult {
138 pub object_id: String,
140 pub passed: bool,
142 pub level: IntegrityLevel,
144 pub errors: Vec<String>,
146 pub duration_us: u64,
148}
149
150#[derive(Debug, Clone)]
152pub struct CheckerConfig {
153 pub default_level: IntegrityLevel,
155 pub auto_verify_on_read: bool,
159 pub corruption_threshold: f64,
162 pub max_objects: usize,
164}
165
166impl Default for CheckerConfig {
167 fn default() -> Self {
168 Self {
169 default_level: IntegrityLevel::Full,
170 auto_verify_on_read: false,
171 corruption_threshold: 0.05,
172 max_objects: 1_000_000,
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
179pub struct CheckerStats {
180 pub total_objects: usize,
182 pub verified_objects: usize,
184 pub corrupted_objects: usize,
186 pub corruption_rate: f64,
188 pub total_verifications: u64,
190 pub avg_verification_us: f64,
192}
193
194#[derive(Debug, Error, PartialEq, Eq)]
196pub enum CheckerError {
197 #[error("object not found: {0}")]
199 ObjectNotFound(String),
200 #[error("hash mismatch for '{id}' on field '{field}'")]
202 HashMismatch {
203 id: String,
205 field: String,
207 },
208 #[error("maximum object capacity exceeded")]
210 MaxObjectsExceeded,
211 #[error("invalid integrity level: {0}")]
213 InvalidLevel(String),
214}
215
216pub struct ObjectIntegrityChecker {
225 config: CheckerConfig,
226 objects: HashMap<String, OicObjectRecord>,
227 total_verify_us: u64,
229 total_verifications: u64,
231}
232
233impl ObjectIntegrityChecker {
234 pub fn new(config: CheckerConfig) -> Self {
236 Self {
237 objects: HashMap::new(),
238 total_verify_us: 0,
239 total_verifications: 0,
240 config,
241 }
242 }
243
244 pub fn with_defaults() -> Self {
246 Self::new(CheckerConfig::default())
247 }
248
249 fn now_us() -> u64 {
254 SystemTime::now()
255 .duration_since(UNIX_EPOCH)
256 .map(|d| d.as_micros() as u64)
257 .unwrap_or(0)
258 }
259
260 fn validate_level(level: &IntegrityLevel) -> Result<(), CheckerError> {
261 if let IntegrityLevel::Custom(algos) = level {
262 for algo in algos {
263 if !IntegrityLevel::SUPPORTED.contains(&algo.as_str()) {
264 return Err(CheckerError::InvalidLevel(format!(
265 "unsupported algorithm '{}'; supported: {:?}",
266 algo,
267 IntegrityLevel::SUPPORTED
268 )));
269 }
270 }
271 }
272 Ok(())
273 }
274
275 fn run_checks(
277 stored: &IntegrityHash,
278 fresh: &IntegrityHash,
279 level: &IntegrityLevel,
280 ) -> Vec<String> {
281 let mut errors = Vec::new();
282
283 if stored.size_bytes != fresh.size_bytes {
285 errors.push(format!(
286 "size mismatch: expected {} bytes, got {}",
287 stored.size_bytes, fresh.size_bytes
288 ));
289 }
290
291 match level {
292 IntegrityLevel::Quick => {
293 if stored.crc16 != fresh.crc16 {
294 errors.push(format!(
295 "crc16 mismatch: expected {:#06x}, got {:#06x}",
296 stored.crc16, fresh.crc16
297 ));
298 }
299 }
300 IntegrityLevel::Standard => {
301 if stored.fnv1a != fresh.fnv1a {
302 errors.push(format!(
303 "fnv1a mismatch: expected {:#018x}, got {:#018x}",
304 stored.fnv1a, fresh.fnv1a
305 ));
306 }
307 if stored.adler32 != fresh.adler32 {
308 errors.push(format!(
309 "adler32 mismatch: expected {:#010x}, got {:#010x}",
310 stored.adler32, fresh.adler32
311 ));
312 }
313 }
314 IntegrityLevel::Full => {
315 if stored.fnv1a != fresh.fnv1a {
316 errors.push(format!(
317 "fnv1a mismatch: expected {:#018x}, got {:#018x}",
318 stored.fnv1a, fresh.fnv1a
319 ));
320 }
321 if stored.adler32 != fresh.adler32 {
322 errors.push(format!(
323 "adler32 mismatch: expected {:#010x}, got {:#010x}",
324 stored.adler32, fresh.adler32
325 ));
326 }
327 if stored.crc16 != fresh.crc16 {
328 errors.push(format!(
329 "crc16 mismatch: expected {:#06x}, got {:#06x}",
330 stored.crc16, fresh.crc16
331 ));
332 }
333 }
334 IntegrityLevel::Custom(algos) => {
335 for algo in algos {
336 match algo.as_str() {
337 "fnv1a" => {
338 if stored.fnv1a != fresh.fnv1a {
339 errors.push(format!(
340 "fnv1a mismatch: expected {:#018x}, got {:#018x}",
341 stored.fnv1a, fresh.fnv1a
342 ));
343 }
344 }
345 "adler32" => {
346 if stored.adler32 != fresh.adler32 {
347 errors.push(format!(
348 "adler32 mismatch: expected {:#010x}, got {:#010x}",
349 stored.adler32, fresh.adler32
350 ));
351 }
352 }
353 "crc16" => {
354 if stored.crc16 != fresh.crc16 {
355 errors.push(format!(
356 "crc16 mismatch: expected {:#06x}, got {:#06x}",
357 stored.crc16, fresh.crc16
358 ));
359 }
360 }
361 _ => {
362 errors.push(format!("unknown algorithm '{algo}' skipped"));
365 }
366 }
367 }
368 }
369 }
370
371 errors
372 }
373
374 pub fn register(&mut self, id: String, data: &[u8]) -> Result<IntegrityHash, CheckerError> {
384 if !self.objects.contains_key(&id) && self.objects.len() >= self.config.max_objects {
385 return Err(CheckerError::MaxObjectsExceeded);
386 }
387 let hash = IntegrityHash::compute(data);
388 let now = Self::now_us();
389 let record = OicObjectRecord {
390 id: id.clone(),
391 hash: hash.clone(),
392 created_at: now,
393 last_verified_at: 0,
394 verification_count: 0,
395 status: OicIntegrityStatus::Unknown,
396 };
397 self.objects.insert(id, record);
398 Ok(hash)
399 }
400
401 pub fn verify(
406 &mut self,
407 id: &str,
408 data: &[u8],
409 level: IntegrityLevel,
410 ) -> Result<OicVerificationResult, CheckerError> {
411 Self::validate_level(&level)?;
412
413 let record = self
414 .objects
415 .get(id)
416 .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
417
418 let stored = record.hash.clone();
419 let t_start = Self::now_us();
420 let fresh = IntegrityHash::compute(data);
421 let errors = Self::run_checks(&stored, &fresh, &level);
422 let t_end = Self::now_us();
423 let duration_us = t_end.saturating_sub(t_start);
424
425 let passed = errors.is_empty();
426 let now = Self::now_us();
427
428 let record = self
430 .objects
431 .get_mut(id)
432 .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
433
434 record.verification_count += 1;
435 self.total_verifications += 1;
436 self.total_verify_us = self.total_verify_us.saturating_add(duration_us);
437
438 if passed {
439 record.last_verified_at = now;
440 record.status = OicIntegrityStatus::Verified(now);
441 } else {
442 record.status = OicIntegrityStatus::Corrupted {
443 detected_at: now,
444 expected: stored.clone(),
445 got: fresh.clone(),
446 };
447 }
448
449 Ok(OicVerificationResult {
450 object_id: id.to_string(),
451 passed,
452 level,
453 errors,
454 duration_us,
455 })
456 }
457
458 pub fn verify_all(
463 &mut self,
464 objects: &[(&str, &[u8])],
465 level: IntegrityLevel,
466 ) -> Vec<OicVerificationResult> {
467 let mut results = Vec::with_capacity(objects.len());
468 for &(id, data) in objects {
469 let result = match self.verify(id, data, level.clone()) {
470 Ok(r) => r,
471 Err(e) => OicVerificationResult {
472 object_id: id.to_string(),
473 passed: false,
474 level: level.clone(),
475 errors: vec![e.to_string()],
476 duration_us: 0,
477 },
478 };
479 results.push(result);
480 }
481 results
482 }
483
484 pub fn mark_corrupted(&mut self, id: &str, reason: String) -> Result<(), CheckerError> {
486 let record = self
487 .objects
488 .get_mut(id)
489 .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
490 record.status = OicIntegrityStatus::PossiblyCorrupted(reason);
491 Ok(())
492 }
493
494 pub fn update(&mut self, id: &str, new_data: &[u8]) -> Result<IntegrityHash, CheckerError> {
497 let record = self
498 .objects
499 .get_mut(id)
500 .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
501 let hash = IntegrityHash::compute(new_data);
502 record.hash = hash.clone();
503 record.verification_count = 0;
504 record.last_verified_at = 0;
505 record.status = OicIntegrityStatus::Unknown;
506 Ok(hash)
507 }
508
509 pub fn remove(&mut self, id: &str) -> Result<(), CheckerError> {
511 self.objects
512 .remove(id)
513 .map(|_| ())
514 .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))
515 }
516
517 pub fn corrupted_objects(&self) -> Vec<&OicObjectRecord> {
519 self.objects
520 .values()
521 .filter(|r| matches!(r.status, OicIntegrityStatus::Corrupted { .. }))
522 .collect()
523 }
524
525 pub fn objects_needing_verification(&self, since_us: u64) -> Vec<&OicObjectRecord> {
529 self.objects
530 .values()
531 .filter(|r| r.last_verified_at < since_us)
532 .collect()
533 }
534
535 pub fn repair_hash(&mut self, id: &str, correct_data: &[u8]) -> Result<(), CheckerError> {
541 let record = self
542 .objects
543 .get_mut(id)
544 .ok_or_else(|| CheckerError::ObjectNotFound(id.to_string()))?;
545 let now = Self::now_us();
546 record.hash = IntegrityHash::compute(correct_data);
547 record.status = OicIntegrityStatus::Verified(now);
548 record.last_verified_at = now;
549 Ok(())
550 }
551
552 pub fn stats(&self) -> CheckerStats {
554 let total_objects = self.objects.len();
555 let verified_objects = self
556 .objects
557 .values()
558 .filter(|r| matches!(r.status, OicIntegrityStatus::Verified(_)))
559 .count();
560 let corrupted_objects = self
561 .objects
562 .values()
563 .filter(|r| matches!(r.status, OicIntegrityStatus::Corrupted { .. }))
564 .count();
565 let corruption_rate = if total_objects == 0 {
566 0.0
567 } else {
568 corrupted_objects as f64 / total_objects as f64
569 };
570 let avg_verification_us = if self.total_verifications == 0 {
571 0.0
572 } else {
573 self.total_verify_us as f64 / self.total_verifications as f64
574 };
575 CheckerStats {
576 total_objects,
577 verified_objects,
578 corrupted_objects,
579 corruption_rate,
580 total_verifications: self.total_verifications,
581 avg_verification_us,
582 }
583 }
584
585 pub fn config(&self) -> &CheckerConfig {
587 &self.config
588 }
589
590 pub fn len(&self) -> usize {
592 self.objects.len()
593 }
594
595 pub fn is_empty(&self) -> bool {
597 self.objects.is_empty()
598 }
599}
600
601#[cfg(test)]
606mod tests {
607 use super::*;
608
609 struct Xorshift64(u64);
614
615 impl Xorshift64 {
616 fn new(seed: u64) -> Self {
617 Self(if seed == 0 { 0xdeadbeef_cafebabe } else { seed })
619 }
620
621 fn next(&mut self) -> u64 {
622 let mut x = self.0;
623 x ^= x << 13;
624 x ^= x >> 7;
625 x ^= x << 17;
626 self.0 = x;
627 x
628 }
629
630 fn next_bytes(&mut self, buf: &mut [u8]) {
631 let mut i = 0;
632 while i < buf.len() {
633 let v = self.next().to_le_bytes();
634 let take = (buf.len() - i).min(8);
635 buf[i..i + take].copy_from_slice(&v[..take]);
636 i += take;
637 }
638 }
639 }
640
641 fn rng_bytes(seed: u64, len: usize) -> Vec<u8> {
642 let mut rng = Xorshift64::new(seed);
643 let mut buf = vec![0u8; len];
644 rng.next_bytes(&mut buf);
645 buf
646 }
647
648 fn checker() -> ObjectIntegrityChecker {
649 ObjectIntegrityChecker::with_defaults()
650 }
651
652 #[test]
657 fn test_fnv1a_empty() {
658 assert_eq!(fnv1a_64(b""), 14695981039346656037u64);
660 }
661
662 #[test]
663 fn test_fnv1a_hello() {
664 let h = fnv1a_64(b"hello");
666 assert_ne!(h, fnv1a_64(b"world"));
667 }
668
669 #[test]
670 fn test_adler32_empty() {
671 assert_eq!(adler32(b""), 1);
673 }
674
675 #[test]
676 fn test_adler32_abc() {
677 assert_eq!(adler32(b"ABC"), 26017991);
682 }
683
684 #[test]
685 fn test_crc16_empty() {
686 assert_eq!(crc16(b""), 0xFFFF);
688 }
689
690 #[test]
691 fn test_crc16_deterministic() {
692 let a = crc16(b"deterministic");
693 let b = crc16(b"deterministic");
694 assert_eq!(a, b);
695 }
696
697 #[test]
698 fn test_crc16_distinct() {
699 assert_ne!(crc16(b"hello"), crc16(b"world"));
700 }
701
702 #[test]
707 fn test_integrity_hash_compute_size() {
708 let data = b"test data";
709 let h = IntegrityHash::compute(data);
710 assert_eq!(h.size_bytes, data.len());
711 }
712
713 #[test]
714 fn test_integrity_hash_equality() {
715 let data = b"consistent";
716 assert_eq!(IntegrityHash::compute(data), IntegrityHash::compute(data));
717 }
718
719 #[test]
720 fn test_integrity_hash_inequality() {
721 assert_ne!(
722 IntegrityHash::compute(b"aaa"),
723 IntegrityHash::compute(b"bbb")
724 );
725 }
726
727 #[test]
732 fn test_register_basic() {
733 let mut c = checker();
734 let data = b"hello world";
735 let h = c.register("obj1".to_string(), data).unwrap();
736 assert_eq!(h, IntegrityHash::compute(data));
737 }
738
739 #[test]
740 fn test_register_stores_record() {
741 let mut c = checker();
742 c.register("a".to_string(), b"payload").unwrap();
743 assert_eq!(c.len(), 1);
744 }
745
746 #[test]
747 fn test_register_overwrite() {
748 let mut c = checker();
749 c.register("x".to_string(), b"v1").unwrap();
750 let h2 = c.register("x".to_string(), b"v2").unwrap();
751 assert_eq!(h2, IntegrityHash::compute(b"v2"));
752 assert_eq!(c.len(), 1);
753 }
754
755 #[test]
756 fn test_register_empty_data() {
757 let mut c = checker();
758 let h = c.register("empty".to_string(), b"").unwrap();
759 assert_eq!(h.size_bytes, 0);
760 }
761
762 #[test]
763 fn test_register_max_objects_exceeded() {
764 let config = CheckerConfig {
765 max_objects: 2,
766 ..Default::default()
767 };
768 let mut c = ObjectIntegrityChecker::new(config);
769 c.register("a".to_string(), b"data").unwrap();
770 c.register("b".to_string(), b"data").unwrap();
771 let err = c.register("c".to_string(), b"data").unwrap_err();
772 assert_eq!(err, CheckerError::MaxObjectsExceeded);
773 }
774
775 #[test]
780 fn test_verify_quick_pass() {
781 let mut c = checker();
782 c.register("q".to_string(), b"data").unwrap();
783 let r = c.verify("q", b"data", IntegrityLevel::Quick).unwrap();
784 assert!(r.passed);
785 assert!(r.errors.is_empty());
786 }
787
788 #[test]
789 fn test_verify_quick_fail() {
790 let mut c = checker();
791 c.register("q".to_string(), b"original").unwrap();
792 let r = c.verify("q", b"tampered", IntegrityLevel::Quick).unwrap();
793 assert!(!r.passed);
794 assert!(!r.errors.is_empty());
795 }
796
797 #[test]
798 fn test_verify_quick_size_mismatch() {
799 let mut c = checker();
800 c.register("q".to_string(), b"hello").unwrap();
801 let r = c.verify("q", b"hi", IntegrityLevel::Quick).unwrap();
802 assert!(!r.passed);
803 }
804
805 #[test]
810 fn test_verify_standard_pass() {
811 let mut c = checker();
812 c.register("s".to_string(), b"payload").unwrap();
813 let r = c.verify("s", b"payload", IntegrityLevel::Standard).unwrap();
814 assert!(r.passed);
815 }
816
817 #[test]
818 fn test_verify_standard_fail_fnv() {
819 let mut c = checker();
820 c.register("s".to_string(), b"original").unwrap();
821 let r = c
822 .verify("s", b"modified", IntegrityLevel::Standard)
823 .unwrap();
824 assert!(!r.passed);
825 }
826
827 #[test]
832 fn test_verify_full_pass() {
833 let mut c = checker();
834 c.register("f".to_string(), b"full check").unwrap();
835 let r = c.verify("f", b"full check", IntegrityLevel::Full).unwrap();
836 assert!(r.passed);
837 assert!(r.errors.is_empty());
838 }
839
840 #[test]
841 fn test_verify_full_fail_multiple_errors() {
842 let mut c = checker();
843 c.register("f".to_string(), b"abcdefgh").unwrap();
844 let r = c.verify("f", b"ABCDEFGH", IntegrityLevel::Full).unwrap();
845 assert!(!r.passed);
846 assert!(
848 r.errors.len() >= 3,
849 "expected ≥3 errors, got {}",
850 r.errors.len()
851 );
852 }
853
854 #[test]
859 fn test_verify_custom_fnv_only_pass() {
860 let mut c = checker();
861 c.register("cust".to_string(), b"data").unwrap();
862 let lvl = IntegrityLevel::Custom(vec!["fnv1a".to_string()]);
863 let r = c.verify("cust", b"data", lvl).unwrap();
864 assert!(r.passed);
865 }
866
867 #[test]
868 fn test_verify_custom_fnv_only_fail() {
869 let mut c = checker();
870 c.register("cust".to_string(), b"data").unwrap();
871 let lvl = IntegrityLevel::Custom(vec!["fnv1a".to_string()]);
872 let r = c.verify("cust", b"DATA", lvl).unwrap();
873 assert!(!r.passed);
874 }
875
876 #[test]
877 fn test_verify_custom_adler32_only() {
878 let mut c = checker();
879 c.register("ad".to_string(), b"check").unwrap();
880 let lvl = IntegrityLevel::Custom(vec!["adler32".to_string()]);
881 let r = c.verify("ad", b"check", lvl).unwrap();
882 assert!(r.passed);
883 }
884
885 #[test]
886 fn test_verify_custom_invalid_algo() {
887 let mut c = checker();
888 c.register("inv".to_string(), b"x").unwrap();
889 let lvl = IntegrityLevel::Custom(vec!["sha256".to_string()]);
890 let err = c.verify("inv", b"x", lvl).unwrap_err();
891 assert!(matches!(err, CheckerError::InvalidLevel(_)));
892 }
893
894 #[test]
895 fn test_verify_custom_all_algos() {
896 let mut c = checker();
897 c.register("all".to_string(), b"alldata").unwrap();
898 let lvl = IntegrityLevel::Custom(vec![
899 "fnv1a".to_string(),
900 "adler32".to_string(),
901 "crc16".to_string(),
902 ]);
903 let r = c.verify("all", b"alldata", lvl).unwrap();
904 assert!(r.passed);
905 }
906
907 #[test]
912 fn test_verify_increments_count() {
913 let mut c = checker();
914 c.register("v".to_string(), b"data").unwrap();
915 c.verify("v", b"data", IntegrityLevel::Full).unwrap();
916 c.verify("v", b"data", IntegrityLevel::Quick).unwrap();
917 let s = c.stats();
918 assert_eq!(s.total_verifications, 2);
919 }
920
921 #[test]
922 fn test_verify_updates_status_on_pass() {
923 let mut c = checker();
924 c.register("u".to_string(), b"data").unwrap();
925 c.verify("u", b"data", IntegrityLevel::Full).unwrap();
926 let s = c.stats();
927 assert_eq!(s.verified_objects, 1);
928 }
929
930 #[test]
931 fn test_verify_updates_status_on_fail() {
932 let mut c = checker();
933 c.register("u".to_string(), b"original").unwrap();
934 c.verify("u", b"tampered", IntegrityLevel::Full).unwrap();
935 let s = c.stats();
936 assert_eq!(s.corrupted_objects, 1);
937 }
938
939 #[test]
940 fn test_verify_not_found() {
941 let mut c = checker();
942 let err = c
943 .verify("ghost", b"data", IntegrityLevel::Quick)
944 .unwrap_err();
945 assert!(matches!(err, CheckerError::ObjectNotFound(_)));
946 }
947
948 #[test]
953 fn test_verify_all_all_pass() {
954 let mut c = checker();
955 c.register("a1".to_string(), b"alpha").unwrap();
956 c.register("b1".to_string(), b"beta").unwrap();
957 let pairs: Vec<(&str, &[u8])> = vec![("a1", b"alpha"), ("b1", b"beta")];
958 let results = c.verify_all(&pairs, IntegrityLevel::Full);
959 assert_eq!(results.len(), 2);
960 assert!(results.iter().all(|r| r.passed));
961 }
962
963 #[test]
964 fn test_verify_all_mixed() {
965 let mut c = checker();
966 c.register("good".to_string(), b"ok").unwrap();
967 c.register("bad".to_string(), b"original").unwrap();
968 let pairs: Vec<(&str, &[u8])> = vec![("good", b"ok"), ("bad", b"tampered")];
969 let results = c.verify_all(&pairs, IntegrityLevel::Full);
970 assert!(results[0].passed);
971 assert!(!results[1].passed);
972 }
973
974 #[test]
975 fn test_verify_all_missing_object() {
976 let mut c = checker();
977 let pairs: Vec<(&str, &[u8])> = vec![("ghost", b"data")];
978 let results = c.verify_all(&pairs, IntegrityLevel::Quick);
979 assert_eq!(results.len(), 1);
980 assert!(!results[0].passed);
981 assert!(!results[0].errors.is_empty());
982 }
983
984 #[test]
985 fn test_verify_all_empty_input() {
986 let mut c = checker();
987 let results = c.verify_all(&[], IntegrityLevel::Full);
988 assert!(results.is_empty());
989 }
990
991 #[test]
992 fn test_verify_all_large_batch() {
993 let mut c = checker();
994 let data: Vec<Vec<u8>> = (0u64..20).map(|i| rng_bytes(i + 1, 64)).collect();
995 let ids: Vec<String> = (0..20).map(|i| format!("obj{i}")).collect();
996 for (id, d) in ids.iter().zip(data.iter()) {
997 c.register(id.clone(), d).unwrap();
998 }
999 let pairs: Vec<(&str, &[u8])> = ids
1000 .iter()
1001 .zip(data.iter())
1002 .map(|(id, d)| (id.as_str(), d.as_slice()))
1003 .collect();
1004 let results = c.verify_all(&pairs, IntegrityLevel::Standard);
1005 assert!(results.iter().all(|r| r.passed));
1006 }
1007
1008 #[test]
1013 fn test_mark_corrupted_sets_status() {
1014 let mut c = checker();
1015 c.register("mc".to_string(), b"data").unwrap();
1016 c.mark_corrupted("mc", "disk error".to_string()).unwrap();
1017 let s = c.stats();
1018 assert_eq!(s.corrupted_objects, 0);
1021 let needing = c.objects_needing_verification(u64::MAX);
1023 assert_eq!(needing.len(), 1);
1024 }
1025
1026 #[test]
1027 fn test_mark_corrupted_not_found() {
1028 let mut c = checker();
1029 let err = c.mark_corrupted("ghost", "reason".to_string()).unwrap_err();
1030 assert!(matches!(err, CheckerError::ObjectNotFound(_)));
1031 }
1032
1033 #[test]
1034 fn test_mark_corrupted_reason_stored() {
1035 let mut c = checker();
1036 c.register("r".to_string(), b"data").unwrap();
1037 c.mark_corrupted("r", "sector read failure".to_string())
1038 .unwrap();
1039 let r = c.verify("r", b"data", IntegrityLevel::Full).unwrap();
1042 assert!(r.passed);
1043 }
1044
1045 #[test]
1050 fn test_update_changes_hash() {
1051 let mut c = checker();
1052 c.register("upd".to_string(), b"v1").unwrap();
1053 let h2 = c.update("upd", b"v2").unwrap();
1054 assert_eq!(h2, IntegrityHash::compute(b"v2"));
1055 }
1056
1057 #[test]
1058 fn test_update_resets_verification_count() {
1059 let mut c = checker();
1060 c.register("upd".to_string(), b"v1").unwrap();
1061 c.verify("upd", b"v1", IntegrityLevel::Full).unwrap();
1062 c.update("upd", b"v2").unwrap();
1063 let r = c.verify("upd", b"v2", IntegrityLevel::Full).unwrap();
1066 assert!(r.passed);
1067 }
1068
1069 #[test]
1070 fn test_update_not_found() {
1071 let mut c = checker();
1072 let err = c.update("ghost", b"data").unwrap_err();
1073 assert!(matches!(err, CheckerError::ObjectNotFound(_)));
1074 }
1075
1076 #[test]
1077 fn test_update_verify_old_data_fails() {
1078 let mut c = checker();
1079 c.register("u2".to_string(), b"old").unwrap();
1080 c.update("u2", b"new").unwrap();
1081 let r = c.verify("u2", b"old", IntegrityLevel::Full).unwrap();
1082 assert!(!r.passed);
1083 }
1084
1085 #[test]
1090 fn test_remove_decrements_count() {
1091 let mut c = checker();
1092 c.register("rm".to_string(), b"data").unwrap();
1093 assert_eq!(c.len(), 1);
1094 c.remove("rm").unwrap();
1095 assert_eq!(c.len(), 0);
1096 }
1097
1098 #[test]
1099 fn test_remove_not_found() {
1100 let mut c = checker();
1101 let err = c.remove("ghost").unwrap_err();
1102 assert!(matches!(err, CheckerError::ObjectNotFound(_)));
1103 }
1104
1105 #[test]
1106 fn test_remove_allows_reregistration() {
1107 let mut c = checker();
1108 c.register("x".to_string(), b"v1").unwrap();
1109 c.remove("x").unwrap();
1110 c.register("x".to_string(), b"v2").unwrap();
1111 let r = c.verify("x", b"v2", IntegrityLevel::Full).unwrap();
1112 assert!(r.passed);
1113 }
1114
1115 #[test]
1120 fn test_corrupted_objects_empty() {
1121 let c = checker();
1122 assert!(c.corrupted_objects().is_empty());
1123 }
1124
1125 #[test]
1126 fn test_corrupted_objects_after_failed_verify() {
1127 let mut c = checker();
1128 c.register("co".to_string(), b"original").unwrap();
1129 c.verify("co", b"tampered", IntegrityLevel::Full).unwrap();
1130 assert_eq!(c.corrupted_objects().len(), 1);
1131 assert_eq!(c.corrupted_objects()[0].id, "co");
1132 }
1133
1134 #[test]
1135 fn test_corrupted_objects_multiple() {
1136 let mut c = checker();
1137 for i in 0..5 {
1138 c.register(format!("obj{i}"), b"data").unwrap();
1139 }
1140 for i in 0..3 {
1141 c.verify(&format!("obj{i}"), b"wrong", IntegrityLevel::Full)
1142 .unwrap();
1143 }
1144 assert_eq!(c.corrupted_objects().len(), 3);
1145 }
1146
1147 #[test]
1152 fn test_objects_needing_verification_all_new() {
1153 let mut c = checker();
1154 c.register("n1".to_string(), b"d").unwrap();
1155 c.register("n2".to_string(), b"d").unwrap();
1156 let needing = c.objects_needing_verification(u64::MAX);
1158 assert_eq!(needing.len(), 2);
1159 }
1160
1161 #[test]
1162 fn test_objects_needing_verification_after_verify() {
1163 let mut c = checker();
1164 c.register("v1".to_string(), b"d").unwrap();
1165 c.verify("v1", b"d", IntegrityLevel::Full).unwrap();
1166 let needing = c.objects_needing_verification(0);
1168 assert_eq!(needing.len(), 0);
1169 }
1170
1171 #[test]
1172 fn test_objects_needing_verification_since_past() {
1173 let mut c = checker();
1174 c.register("past".to_string(), b"d").unwrap();
1175 let needing = c.objects_needing_verification(1);
1177 assert_eq!(needing.len(), 1);
1178 }
1179
1180 #[test]
1185 fn test_repair_hash_clears_corruption() {
1186 let mut c = checker();
1187 c.register("rh".to_string(), b"original").unwrap();
1188 c.verify("rh", b"tampered", IntegrityLevel::Full).unwrap();
1189 assert_eq!(c.corrupted_objects().len(), 1);
1190 c.repair_hash("rh", b"correct").unwrap();
1191 assert_eq!(c.corrupted_objects().len(), 0);
1192 }
1193
1194 #[test]
1195 fn test_repair_hash_verify_with_correct_data() {
1196 let mut c = checker();
1197 c.register("rh2".to_string(), b"old").unwrap();
1198 c.repair_hash("rh2", b"new_correct").unwrap();
1199 let r = c
1200 .verify("rh2", b"new_correct", IntegrityLevel::Full)
1201 .unwrap();
1202 assert!(r.passed);
1203 }
1204
1205 #[test]
1206 fn test_repair_hash_not_found() {
1207 let mut c = checker();
1208 let err = c.repair_hash("ghost", b"data").unwrap_err();
1209 assert!(matches!(err, CheckerError::ObjectNotFound(_)));
1210 }
1211
1212 #[test]
1217 fn test_stats_empty() {
1218 let c = checker();
1219 let s = c.stats();
1220 assert_eq!(s.total_objects, 0);
1221 assert_eq!(s.verified_objects, 0);
1222 assert_eq!(s.corrupted_objects, 0);
1223 assert_eq!(s.corruption_rate, 0.0);
1224 assert_eq!(s.total_verifications, 0);
1225 assert_eq!(s.avg_verification_us, 0.0);
1226 }
1227
1228 #[test]
1229 fn test_stats_after_registration() {
1230 let mut c = checker();
1231 c.register("s1".to_string(), b"a").unwrap();
1232 c.register("s2".to_string(), b"b").unwrap();
1233 let s = c.stats();
1234 assert_eq!(s.total_objects, 2);
1235 assert_eq!(s.verified_objects, 0);
1236 }
1237
1238 #[test]
1239 fn test_stats_corruption_rate() {
1240 let mut c = checker();
1241 c.register("s1".to_string(), b"a").unwrap();
1242 c.register("s2".to_string(), b"b").unwrap();
1243 c.verify("s1", b"tampered", IntegrityLevel::Full).unwrap();
1244 let s = c.stats();
1245 assert_eq!(s.corrupted_objects, 1);
1246 assert!((s.corruption_rate - 0.5).abs() < 1e-9);
1247 }
1248
1249 #[test]
1250 fn test_stats_avg_verification_us_non_negative() {
1251 let mut c = checker();
1252 c.register("t1".to_string(), b"data").unwrap();
1253 c.verify("t1", b"data", IntegrityLevel::Full).unwrap();
1254 let s = c.stats();
1255 assert!(s.avg_verification_us >= 0.0);
1256 }
1257
1258 #[test]
1259 fn test_stats_total_verifications() {
1260 let mut c = checker();
1261 c.register("tv".to_string(), b"data").unwrap();
1262 for _ in 0..5 {
1263 c.verify("tv", b"data", IntegrityLevel::Quick).unwrap();
1264 }
1265 let s = c.stats();
1266 assert_eq!(s.total_verifications, 5);
1267 }
1268
1269 #[test]
1274 fn test_error_display_not_found() {
1275 let e = CheckerError::ObjectNotFound("xyz".to_string());
1276 let msg = e.to_string();
1277 assert!(msg.contains("xyz"));
1278 }
1279
1280 #[test]
1281 fn test_error_display_max_exceeded() {
1282 let e = CheckerError::MaxObjectsExceeded;
1283 let msg = e.to_string();
1284 assert!(msg.contains("maximum"));
1285 }
1286
1287 #[test]
1288 fn test_error_display_invalid_level() {
1289 let e = CheckerError::InvalidLevel("bad algo".to_string());
1290 let msg = e.to_string();
1291 assert!(msg.contains("invalid"));
1292 }
1293
1294 #[test]
1299 fn test_large_data_verify() {
1300 let mut c = checker();
1301 let data = rng_bytes(42, 1_024 * 1_024);
1302 c.register("large".to_string(), &data).unwrap();
1303 let r = c.verify("large", &data, IntegrityLevel::Full).unwrap();
1304 assert!(r.passed);
1305 }
1306
1307 #[test]
1308 fn test_many_objects_no_collision() {
1309 let mut c = checker();
1310 for i in 0u64..100 {
1311 let d = rng_bytes(i, 32);
1312 c.register(format!("obj{i}"), &d).unwrap();
1313 }
1314 let s = c.stats();
1315 assert_eq!(s.total_objects, 100);
1316 }
1317
1318 #[test]
1319 fn test_verify_returns_level_in_result() {
1320 let mut c = checker();
1321 c.register("lvl".to_string(), b"d").unwrap();
1322 let r = c.verify("lvl", b"d", IntegrityLevel::Standard).unwrap();
1323 assert_eq!(r.level, IntegrityLevel::Standard);
1324 }
1325
1326 #[test]
1327 fn test_repair_then_verify_large() {
1328 let mut c = checker();
1329 let original = rng_bytes(10, 512);
1330 let corrupted = rng_bytes(99, 512);
1331 let correct = rng_bytes(7, 512);
1332 c.register("repair_large".to_string(), &original).unwrap();
1333 c.verify("repair_large", &corrupted, IntegrityLevel::Full)
1334 .unwrap();
1335 c.repair_hash("repair_large", &correct).unwrap();
1336 let r = c
1337 .verify("repair_large", &correct, IntegrityLevel::Full)
1338 .unwrap();
1339 assert!(r.passed);
1340 }
1341
1342 #[test]
1343 fn test_is_empty() {
1344 let mut c = checker();
1345 assert!(c.is_empty());
1346 c.register("x".to_string(), b"d").unwrap();
1347 assert!(!c.is_empty());
1348 c.remove("x").unwrap();
1349 assert!(c.is_empty());
1350 }
1351
1352 #[test]
1353 fn test_config_accessor() {
1354 let c = checker();
1355 assert_eq!(c.config().max_objects, 1_000_000);
1356 }
1357
1358 #[test]
1359 fn test_verify_duration_recorded() {
1360 let mut c = checker();
1361 c.register("dur".to_string(), b"data").unwrap();
1362 let r = c.verify("dur", b"data", IntegrityLevel::Full).unwrap();
1363 assert!(r.duration_us < u64::MAX);
1365 }
1366
1367 #[test]
1368 fn test_custom_empty_algo_list() {
1369 let mut c = checker();
1370 c.register("emp".to_string(), b"data").unwrap();
1371 let lvl = IntegrityLevel::Custom(vec![]);
1373 let r = c.verify("emp", b"data", lvl).unwrap();
1374 assert!(r.passed);
1375 }
1376
1377 #[test]
1378 fn test_size_mismatch_always_detected() {
1379 let mut c = checker();
1380 c.register("sz".to_string(), b"hello").unwrap();
1381 let r = c.verify("sz", b"hi", IntegrityLevel::Quick).unwrap();
1383 assert!(!r.passed);
1384 let has_size_error = r.errors.iter().any(|e| e.contains("size"));
1385 assert!(has_size_error);
1386 }
1387
1388 #[test]
1389 fn test_verify_object_id_in_result() {
1390 let mut c = checker();
1391 c.register("myid".to_string(), b"data").unwrap();
1392 let r = c.verify("myid", b"data", IntegrityLevel::Quick).unwrap();
1393 assert_eq!(r.object_id, "myid");
1394 }
1395}