1#[derive(Clone, Debug, PartialEq)]
8pub enum IntegrityError {
9 CidMismatch {
11 stored_cid: String,
12 computed_cid: String,
13 },
14 EmptyBlock { cid: String },
16 InvalidCidFormat { cid: String, reason: String },
18 HashFunctionUnsupported { function_code: u64 },
20}
21
22#[derive(Clone, Debug, PartialEq)]
24pub struct CheckedBlock {
25 pub cid: String,
27 pub size_bytes: usize,
29 pub status: CheckStatus,
31}
32
33#[derive(Clone, Debug, PartialEq)]
35pub enum CheckStatus {
36 Valid,
38 Invalid(IntegrityError),
40 Skipped { reason: String },
42}
43
44#[derive(Clone, Debug)]
46pub struct CheckerConfig {
47 pub skip_empty: bool,
49 pub max_blocks: Option<usize>,
51 pub hash_fn: HashFunction,
53}
54
55impl Default for CheckerConfig {
56 fn default() -> Self {
57 Self {
58 skip_empty: false,
59 max_blocks: None,
60 hash_fn: HashFunction::Sha256,
61 }
62 }
63}
64
65#[derive(Clone, Debug, PartialEq)]
67pub enum HashFunction {
68 Sha256,
70 Blake3,
72 Identity,
75}
76
77#[derive(Debug)]
79pub struct IntegrityReport {
80 pub total_checked: usize,
82 pub valid: usize,
84 pub invalid: usize,
86 pub skipped: usize,
88 pub results: Vec<CheckedBlock>,
90}
91
92impl IntegrityReport {
93 pub fn error_rate(&self) -> f64 {
96 if self.total_checked == 0 {
97 0.0
98 } else {
99 self.invalid as f64 / self.total_checked as f64
100 }
101 }
102
103 pub fn invalid_cids(&self) -> Vec<&str> {
105 self.results
106 .iter()
107 .filter(|b| matches!(b.status, CheckStatus::Invalid(_)))
108 .map(|b| b.cid.as_str())
109 .collect()
110 }
111}
112
113fn fnv1a_64(data: &[u8]) -> u64 {
119 const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
120 const PRIME: u64 = 1_099_511_628_211;
121 let mut hash = OFFSET_BASIS;
122 for &byte in data {
123 hash ^= byte as u64;
124 hash = hash.wrapping_mul(PRIME);
125 }
126 hash
127}
128
129pub struct BlockIntegrityChecker {
136 pub config: CheckerConfig,
138}
139
140impl BlockIntegrityChecker {
141 pub fn new(config: CheckerConfig) -> Self {
143 Self { config }
144 }
145
146 pub fn verify_cid_format(&self, cid: &str) -> Result<(), IntegrityError> {
151 if cid.is_empty() {
152 return Err(IntegrityError::InvalidCidFormat {
153 cid: cid.to_string(),
154 reason: "empty".to_string(),
155 });
156 }
157 Ok(())
158 }
159
160 fn compute_cid(&self, data: &[u8]) -> String {
162 match self.config.hash_fn {
163 HashFunction::Identity => {
164 let len = data.len().min(8);
166 let hex_str: String = data[..len].iter().map(|b| format!("{:02x}", b)).collect();
167 format!("identity:{}", hex_str)
168 }
169 HashFunction::Sha256 => {
170 let hash = fnv1a_64(data);
172 format!("bafy{:016x}", hash)
173 }
174 HashFunction::Blake3 => {
175 let mut reversed = data.to_vec();
177 reversed.reverse();
178 let hash = fnv1a_64(&reversed);
179 format!("bafk{:016x}", hash)
180 }
181 }
182 }
183
184 pub fn check_block(&self, cid: &str, data: &[u8]) -> CheckedBlock {
190 if data.is_empty() {
192 if self.config.skip_empty {
193 return CheckedBlock {
194 cid: cid.to_string(),
195 size_bytes: 0,
196 status: CheckStatus::Skipped {
197 reason: "empty block".to_string(),
198 },
199 };
200 } else {
201 return CheckedBlock {
202 cid: cid.to_string(),
203 size_bytes: 0,
204 status: CheckStatus::Invalid(IntegrityError::EmptyBlock {
205 cid: cid.to_string(),
206 }),
207 };
208 }
209 }
210
211 if let Err(e) = self.verify_cid_format(cid) {
213 return CheckedBlock {
214 cid: cid.to_string(),
215 size_bytes: data.len(),
216 status: CheckStatus::Invalid(e),
217 };
218 }
219
220 let computed = self.compute_cid(data);
222 let status = if cid == computed {
223 CheckStatus::Valid
224 } else {
225 CheckStatus::Invalid(IntegrityError::CidMismatch {
226 stored_cid: cid.to_string(),
227 computed_cid: computed,
228 })
229 };
230
231 CheckedBlock {
232 cid: cid.to_string(),
233 size_bytes: data.len(),
234 status,
235 }
236 }
237
238 pub fn check_blocks(&self, blocks: &[(&str, &[u8])]) -> IntegrityReport {
242 let limit = self
243 .config
244 .max_blocks
245 .unwrap_or(usize::MAX)
246 .min(blocks.len());
247
248 let mut results = Vec::with_capacity(limit);
249 let mut valid = 0usize;
250 let mut invalid = 0usize;
251 let mut skipped = 0usize;
252
253 for (cid, data) in blocks.iter().take(limit) {
254 let checked = self.check_block(cid, data);
255 match &checked.status {
256 CheckStatus::Valid => valid += 1,
257 CheckStatus::Invalid(_) => invalid += 1,
258 CheckStatus::Skipped { .. } => skipped += 1,
259 }
260 results.push(checked);
261 }
262
263 IntegrityReport {
264 total_checked: results.len(),
265 valid,
266 invalid,
267 skipped,
268 results,
269 }
270 }
271}
272
273#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn default_checker() -> BlockIntegrityChecker {
282 BlockIntegrityChecker::new(CheckerConfig::default())
283 }
284
285 fn sha256_checker() -> BlockIntegrityChecker {
286 BlockIntegrityChecker::new(CheckerConfig {
287 hash_fn: HashFunction::Sha256,
288 ..CheckerConfig::default()
289 })
290 }
291
292 fn identity_checker() -> BlockIntegrityChecker {
293 BlockIntegrityChecker::new(CheckerConfig {
294 hash_fn: HashFunction::Identity,
295 ..CheckerConfig::default()
296 })
297 }
298
299 fn blake3_checker() -> BlockIntegrityChecker {
300 BlockIntegrityChecker::new(CheckerConfig {
301 hash_fn: HashFunction::Blake3,
302 ..CheckerConfig::default()
303 })
304 }
305
306 #[test]
308 fn test_new_default_config() {
309 let checker = default_checker();
310 assert!(!checker.config.skip_empty);
311 assert!(checker.config.max_blocks.is_none());
312 assert_eq!(checker.config.hash_fn, HashFunction::Sha256);
313 }
314
315 #[test]
317 fn test_check_block_empty_skip_false() {
318 let checker = default_checker();
319 let result = checker.check_block("bafytest", &[]);
320 assert_eq!(
321 result.status,
322 CheckStatus::Invalid(IntegrityError::EmptyBlock {
323 cid: "bafytest".to_string()
324 })
325 );
326 }
327
328 #[test]
330 fn test_check_block_empty_skip_true() {
331 let checker = BlockIntegrityChecker::new(CheckerConfig {
332 skip_empty: true,
333 ..CheckerConfig::default()
334 });
335 let result = checker.check_block("bafytest", &[]);
336 assert!(matches!(result.status, CheckStatus::Skipped { .. }));
337 }
338
339 #[test]
341 fn test_check_block_identity_matches() {
342 let checker = identity_checker();
343 let data = b"hello world";
344 let cid = checker.compute_cid(data);
345 let result = checker.check_block(&cid, data);
346 assert_eq!(result.status, CheckStatus::Valid);
347 }
348
349 #[test]
351 fn test_check_block_identity_mismatch() {
352 let checker = identity_checker();
353 let data = b"hello world";
354 let result = checker.check_block("identity:wrongvalue", data);
355 assert!(matches!(
356 result.status,
357 CheckStatus::Invalid(IntegrityError::CidMismatch { .. })
358 ));
359 }
360
361 #[test]
363 fn test_check_block_sha256_prefix() {
364 let checker = sha256_checker();
365 let data = b"some block data";
366 let cid = checker.compute_cid(data);
367 assert!(
368 cid.starts_with("bafy"),
369 "CID should start with 'bafy', got: {}",
370 cid
371 );
372 }
373
374 #[test]
376 fn test_check_block_blake3_prefix() {
377 let checker = blake3_checker();
378 let data = b"some block data";
379 let cid = checker.compute_cid(data);
380 assert!(
381 cid.starts_with("bafk"),
382 "CID should start with 'bafk', got: {}",
383 cid
384 );
385 }
386
387 #[test]
389 fn test_check_blocks_counts() {
390 let checker = sha256_checker();
391 let data1 = b"block one";
392 let data2 = b"block two";
393 let cid1 = checker.compute_cid(data1);
394 let cid2 = checker.compute_cid(data2);
395 let blocks: Vec<(&str, &[u8])> = vec![
396 (cid1.as_str(), data1.as_ref()),
397 (cid2.as_str(), data2.as_ref()),
398 ];
399 let report = checker.check_blocks(&blocks);
400 assert_eq!(report.total_checked, 2);
401 assert_eq!(report.valid, 2);
402 assert_eq!(report.invalid, 0);
403 assert_eq!(report.skipped, 0);
404 }
405
406 #[test]
408 fn test_check_blocks_max_blocks() {
409 let checker = BlockIntegrityChecker::new(CheckerConfig {
410 max_blocks: Some(2),
411 hash_fn: HashFunction::Sha256,
412 ..CheckerConfig::default()
413 });
414 let data: &[u8] = b"data";
415 let cid = checker.compute_cid(data);
416 let blocks: Vec<(&str, &[u8])> = vec![
417 (cid.as_str(), data),
418 (cid.as_str(), data),
419 (cid.as_str(), data),
420 (cid.as_str(), data),
421 ];
422 let report = checker.check_blocks(&blocks);
423 assert_eq!(report.total_checked, 2);
424 }
425
426 #[test]
428 fn test_check_blocks_mixed() {
429 let checker = sha256_checker();
430 let data = b"real data";
431 let good_cid = checker.compute_cid(data);
432 let blocks: Vec<(&str, &[u8])> = vec![
433 (good_cid.as_str(), data.as_ref()),
434 ("bafybadcid", data.as_ref()),
435 ];
436 let report = checker.check_blocks(&blocks);
437 assert_eq!(report.valid, 1);
438 assert_eq!(report.invalid, 1);
439 }
440
441 #[test]
443 fn test_error_rate() {
444 let checker = sha256_checker();
445 let data = b"some data";
446 let good_cid = checker.compute_cid(data);
447 let blocks: Vec<(&str, &[u8])> = vec![
448 (good_cid.as_str(), data.as_ref()),
449 ("bafybad1", data.as_ref()),
450 ("bafybad2", data.as_ref()),
451 ("bafybad3", data.as_ref()),
452 ];
453 let report = checker.check_blocks(&blocks);
454 let rate = report.error_rate();
455 assert!(
456 (rate - 0.75).abs() < f64::EPSILON,
457 "Expected 0.75, got {}",
458 rate
459 );
460 }
461
462 #[test]
464 fn test_error_rate_zero_blocks() {
465 let report = IntegrityReport {
466 total_checked: 0,
467 valid: 0,
468 invalid: 0,
469 skipped: 0,
470 results: vec![],
471 };
472 assert_eq!(report.error_rate(), 0.0);
473 }
474
475 #[test]
477 fn test_invalid_cids() {
478 let checker = sha256_checker();
479 let data = b"some data";
480 let good_cid = checker.compute_cid(data);
481 let blocks: Vec<(&str, &[u8])> = vec![
482 (good_cid.as_str(), data.as_ref()),
483 ("bafybad_a", data.as_ref()),
484 ("bafybad_b", data.as_ref()),
485 ];
486 let report = checker.check_blocks(&blocks);
487 let mut bad = report.invalid_cids();
488 bad.sort_unstable();
489 assert_eq!(bad, vec!["bafybad_a", "bafybad_b"]);
490 }
491
492 #[test]
494 fn test_verify_cid_format_empty() {
495 let checker = default_checker();
496 let result = checker.verify_cid_format("");
497 assert!(result.is_err());
498 assert!(matches!(
499 result.unwrap_err(),
500 IntegrityError::InvalidCidFormat { .. }
501 ));
502 }
503
504 #[test]
506 fn test_verify_cid_format_nonempty() {
507 let checker = default_checker();
508 assert!(checker
509 .verify_cid_format("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
510 .is_ok());
511 assert!(checker
512 .verify_cid_format("QmPK1s3pNYLi9ERiq3BDxKa4XosgWwFRQUydHUtz4YgpqB")
513 .is_ok());
514 }
515
516 #[test]
518 fn test_identical_data_same_cid() {
519 let checker = sha256_checker();
520 let data = b"deterministic test data";
521 let cid1 = checker.compute_cid(data);
522 let cid2 = checker.compute_cid(data);
523 assert_eq!(cid1, cid2);
524 }
525
526 #[test]
528 fn test_different_data_different_cids() {
529 let checker = sha256_checker();
530 let cid_a = checker.compute_cid(b"data alpha");
531 let cid_b = checker.compute_cid(b"data beta");
532 assert_ne!(cid_a, cid_b);
533 }
534
535 #[test]
537 fn test_sum_equals_total() {
538 let checker = BlockIntegrityChecker::new(CheckerConfig {
539 skip_empty: true,
540 hash_fn: HashFunction::Sha256,
541 ..CheckerConfig::default()
542 });
543 let data = b"real data";
544 let good_cid = checker.compute_cid(data);
545 let blocks: Vec<(&str, &[u8])> = vec![
546 (good_cid.as_str(), data.as_ref()), ("bafybad", data.as_ref()), ("bafyempty", b""), ];
550 let report = checker.check_blocks(&blocks);
551 assert_eq!(
552 report.valid + report.invalid + report.skipped,
553 report.total_checked
554 );
555 assert_eq!(report.total_checked, 3);
556 assert_eq!(report.valid, 1);
557 assert_eq!(report.invalid, 1);
558 assert_eq!(report.skipped, 1);
559 }
560}