1use std::collections::HashMap;
2use std::io::{self, Read, Seek};
3use std::path::PathBuf;
4
5use crate::checksum;
6use crate::md5_simd;
7use crate::par2_set::Par2FileSet;
8use crate::types::{
9 CancellationToken, FileId, MAX_SLICES_PER_FILE, ProgressCallback, ProgressPhase, ProgressStage,
10 ProgressUpdate, SliceChecksum,
11};
12
13const VERIFY_SLICE_CHUNK_BYTES: usize = 64 * 1024;
14const VERIFY_SIMD_BATCH_MEMORY_BYTES: usize = 4 * 1024 * 1024;
15const VERIFY_SIMD_MAX_LANES: usize = 8;
19const QUICK_CHECK_16K_BYTES: usize = 16 * 1024;
20const VERIFY_FULL_HASH_CHUNK_BYTES: usize = 1024 * 1024;
21const VERIFY_SPAN_TARGET_BYTES: usize = 4 * 1024 * 1024;
23
24pub trait FileRangeReader: Read + Seek {}
27
28impl<T: Read + Seek> FileRangeReader for T {}
29
30pub trait FileAccess {
32 fn read_file_range(&self, file_id: &FileId, offset: u64, len: u64) -> io::Result<Vec<u8>>;
34
35 fn read_file_range_into(
37 &self,
38 file_id: &FileId,
39 offset: u64,
40 dst: &mut [u8],
41 ) -> io::Result<usize> {
42 let data = self.read_file_range(file_id, offset, dst.len() as u64)?;
43 let read_len = data.len().min(dst.len());
44 dst[..read_len].copy_from_slice(&data[..read_len]);
45 Ok(read_len)
46 }
47
48 fn open_sequential_reader(&self, _file_id: &FileId) -> io::Result<Option<Box<dyn Read>>> {
50 Ok(None)
51 }
52
53 fn open_range_reader(&self, _file_id: &FileId) -> io::Result<Option<Box<dyn FileRangeReader>>> {
55 Ok(None)
56 }
57
58 fn file_exists(&self, file_id: &FileId) -> bool;
60
61 fn file_length(&self, file_id: &FileId) -> Option<u64>;
63
64 fn read_file(&self, file_id: &FileId) -> io::Result<Vec<u8>>;
66
67 fn write_file_range(&mut self, file_id: &FileId, offset: u64, data: &[u8]) -> io::Result<()>;
69}
70
71pub struct MemoryFileAccess {
73 files: HashMap<FileId, Vec<u8>>,
74}
75
76impl MemoryFileAccess {
77 pub fn new() -> Self {
78 Self {
79 files: HashMap::new(),
80 }
81 }
82
83 pub fn add_file(&mut self, file_id: FileId, data: Vec<u8>) {
84 self.files.insert(file_id, data);
85 }
86}
87
88impl Default for MemoryFileAccess {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl FileAccess for MemoryFileAccess {
95 fn read_file_range(&self, file_id: &FileId, offset: u64, len: u64) -> io::Result<Vec<u8>> {
96 let data = self
97 .files
98 .get(file_id)
99 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))?;
100 let offset = offset as usize;
101 let end = (offset + len as usize).min(data.len());
102 if offset >= data.len() {
103 return Ok(Vec::new());
104 }
105 Ok(data[offset..end].to_vec())
106 }
107
108 fn read_file_range_into(
109 &self,
110 file_id: &FileId,
111 offset: u64,
112 dst: &mut [u8],
113 ) -> io::Result<usize> {
114 let data = self
115 .files
116 .get(file_id)
117 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))?;
118 let offset = offset as usize;
119 if offset >= data.len() {
120 return Ok(0);
121 }
122 let end = (offset + dst.len()).min(data.len());
123 let read_len = end - offset;
124 dst[..read_len].copy_from_slice(&data[offset..end]);
125 Ok(read_len)
126 }
127
128 fn file_exists(&self, file_id: &FileId) -> bool {
129 self.files.contains_key(file_id)
130 }
131
132 fn file_length(&self, file_id: &FileId) -> Option<u64> {
133 self.files.get(file_id).map(|d| d.len() as u64)
134 }
135
136 fn read_file(&self, file_id: &FileId) -> io::Result<Vec<u8>> {
137 self.files
138 .get(file_id)
139 .cloned()
140 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))
141 }
142
143 fn write_file_range(&mut self, file_id: &FileId, offset: u64, data: &[u8]) -> io::Result<()> {
144 let file_data = self
145 .files
146 .get_mut(file_id)
147 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))?;
148 let offset = offset as usize;
149 let end = offset + data.len();
150 if end > file_data.len() {
152 file_data.resize(end, 0);
153 }
154 file_data[offset..end].copy_from_slice(data);
155 Ok(())
156 }
157}
158
159#[derive(Debug, Clone)]
161pub enum FileStatus {
162 Complete,
164 Damaged(u32),
166 Missing,
168 Renamed(PathBuf),
170}
171
172#[derive(Debug, Clone)]
174pub struct FileVerification {
175 pub file_id: FileId,
176 pub filename: String,
177 pub status: FileStatus,
178 pub valid_slices: Vec<bool>,
180 pub missing_slice_count: u32,
182}
183
184#[derive(Debug, Clone)]
186pub enum Repairability {
187 NotNeeded,
189 Repairable {
191 blocks_needed: u32,
192 blocks_available: u32,
193 },
194 Insufficient {
196 blocks_needed: u32,
197 blocks_available: u32,
198 deficit: u32,
199 },
200 ResourceLimited { reason: String },
202}
203
204#[derive(Debug, Clone)]
206pub struct VerificationResult {
207 pub files: Vec<FileVerification>,
208 pub recovery_blocks_available: u32,
212 pub total_missing_blocks: u32,
213 pub repairable: Repairability,
214}
215
216impl VerificationResult {
217 pub fn needs_repair(&self) -> bool {
218 self.total_missing_blocks > 0
219 || self
220 .files
221 .iter()
222 .any(|file| !matches!(file.status, FileStatus::Complete))
223 }
224
225 pub fn refresh_repairability(&mut self) {
226 if let Repairability::ResourceLimited { .. } = self.repairable
227 && (self.total_missing_blocks > 0
228 || self
229 .files
230 .iter()
231 .any(|file| !matches!(file.status, FileStatus::Complete)))
232 {
233 return;
234 }
235
236 self.repairable = repairability_for_result(
237 &self.files,
238 self.total_missing_blocks,
239 self.recovery_blocks_available,
240 );
241 }
242}
243
244fn repairability_for_result(
245 files: &[FileVerification],
246 total_missing_blocks: u32,
247 recovery_blocks_available: u32,
248) -> Repairability {
249 if total_missing_blocks == 0
250 && files
251 .iter()
252 .all(|file| matches!(file.status, FileStatus::Complete))
253 {
254 Repairability::NotNeeded
255 } else {
256 repairability_for_counts(total_missing_blocks, recovery_blocks_available)
257 }
258}
259
260fn repairability_for_result_with_resource_limit(
261 files: &[FileVerification],
262 total_missing_blocks: u32,
263 recovery_blocks_available: u32,
264 resource_limit_reason: Option<String>,
265) -> Repairability {
266 match resource_limit_reason {
267 Some(reason) => Repairability::ResourceLimited { reason },
268 None => repairability_for_result(files, total_missing_blocks, recovery_blocks_available),
269 }
270}
271
272fn repairability_for_counts(
273 total_missing_blocks: u32,
274 recovery_blocks_available: u32,
275) -> Repairability {
276 if total_missing_blocks == 0 {
277 Repairability::NotNeeded
278 } else if total_missing_blocks <= recovery_blocks_available {
279 Repairability::Repairable {
280 blocks_needed: total_missing_blocks,
281 blocks_available: recovery_blocks_available,
282 }
283 } else {
284 Repairability::Insufficient {
285 blocks_needed: total_missing_blocks,
286 blocks_available: recovery_blocks_available,
287 deficit: total_missing_blocks - recovery_blocks_available,
288 }
289 }
290}
291
292fn bounded_slice_count(par2: &Par2FileSet, length: u64) -> Option<usize> {
293 let count = usize::try_from(par2.slice_count_for_file(length)).ok()?;
294 (count <= MAX_SLICES_PER_FILE).then_some(count)
295}
296
297fn resource_limited_verification(file_id: FileId, filename: String) -> FileVerification {
298 FileVerification {
299 file_id,
300 filename,
301 status: FileStatus::Damaged(0),
302 valid_slices: Vec::new(),
303 missing_slice_count: 0,
304 }
305}
306
307pub fn quick_check_16k(
312 par2: &Par2FileSet,
313 file_id: &FileId,
314 access: &dyn FileAccess,
315) -> Option<bool> {
316 let mut scratch = [0u8; QUICK_CHECK_16K_BYTES];
317 quick_check_16k_with_scratch(par2, file_id, access, &mut scratch)
318}
319
320fn quick_check_16k_with_scratch(
321 par2: &Par2FileSet,
322 file_id: &FileId,
323 access: &dyn FileAccess,
324 scratch: &mut [u8; QUICK_CHECK_16K_BYTES],
325) -> Option<bool> {
326 let desc = par2.file_description(file_id)?;
327
328 if !access.file_exists(file_id) {
329 return Some(false);
330 }
331
332 let read_len = if let Some(mut reader) = access.open_sequential_reader(file_id).ok()? {
333 let scratch_len = scratch.len();
334 read_from_sequential_reader(&mut *reader, scratch, scratch_len).ok()?
335 } else {
336 access.read_file_range_into(file_id, 0, scratch).ok()?
337 };
338 let hash = checksum::md5(&scratch[..read_len]);
339 Some(hash == desc.hash_16k)
340}
341
342pub fn verify_full_hash(
344 par2: &Par2FileSet,
345 file_id: &FileId,
346 access: &dyn FileAccess,
347) -> Option<bool> {
348 let desc = par2.file_description(file_id)?;
349 let actual_len = access.file_length(file_id)?;
350 if actual_len != desc.length {
351 return Some(false);
352 }
353 verify_full_hash_streaming(desc.hash_full, actual_len, file_id, access)
354}
355
356enum StrictStreamOutcome {
358 SliceCrcMismatch,
363 Hashes { quick_ok: bool, full_ok: bool },
366}
367
368fn fused_chunk_bytes(slice_size: u64) -> Option<usize> {
376 let slice = usize::try_from(slice_size).ok()?;
377 if slice == 0 || slice > VERIFY_FULL_HASH_CHUNK_BYTES {
378 return None;
379 }
380 Some((VERIFY_FULL_HASH_CHUNK_BYTES / slice) * slice)
381}
382
383fn stream_strict_hashes(
398 par2: &Par2FileSet,
399 file_id: &FileId,
400 access: &dyn FileAccess,
401 slice_checksums: Option<&[SliceChecksum]>,
402) -> Option<StrictStreamOutcome> {
403 let desc = par2.file_description(file_id)?;
404 let actual_len = access.file_length(file_id)?;
405 let slice_size = par2.slice_size;
406 let fused = slice_checksums.zip(fused_chunk_bytes(slice_size));
409 let chunk_bytes = fused.map_or(VERIFY_FULL_HASH_CHUNK_BYTES, |(_, chunk)| chunk);
410
411 let mut quick_state = checksum::FileHashState::new();
412 let mut full_state = None;
413 let mut buf = vec![0u8; chunk_bytes];
414 let mut total_read = 0u64;
415 let mut reader = access.open_sequential_reader(file_id).ok()?;
416
417 while total_read < actual_len {
418 let want = ((actual_len - total_read) as usize).min(chunk_bytes);
419 let read_len = match reader.as_mut() {
420 Some(reader) => {
423 read_from_sequential_reader(&mut **reader, &mut buf[..want], want).ok()?
424 }
425 None => access
426 .read_file_range_into(file_id, total_read, &mut buf[..want])
427 .ok()?,
428 };
429 if read_len == 0 {
430 break;
431 }
432 let data = &buf[..read_len];
433 update_quick_and_full_hash_states(&mut quick_state, &mut full_state, data);
434 if let Some((checksums, _)) = fused
435 && chunk_proves_slice_damage(data, total_read, slice_size, actual_len, checksums)
436 {
437 return Some(StrictStreamOutcome::SliceCrcMismatch);
438 }
439 total_read += read_len as u64;
440 if read_len < want {
441 break;
442 }
443 }
444
445 let grew = match reader.as_mut() {
449 Some(reader) => {
450 let mut probe = [0u8; 1];
451 read_from_sequential_reader(&mut **reader, &mut probe, 1).ok()? != 0
452 }
453 None => false,
454 };
455 if total_read != actual_len || grew {
456 return Some(StrictStreamOutcome::Hashes {
457 quick_ok: false,
458 full_ok: false,
459 });
460 }
461
462 let quick_hash = quick_state.finalize();
463 let full_hash = full_state.map_or(quick_hash, checksum::FileHashState::finalize);
464 Some(StrictStreamOutcome::Hashes {
465 quick_ok: quick_hash == desc.hash_16k,
466 full_ok: actual_len == desc.length && full_hash == desc.hash_full,
467 })
468}
469
470fn chunk_proves_slice_damage(
479 data: &[u8],
480 chunk_offset: u64,
481 slice_size: u64,
482 file_len: u64,
483 checksums: &[SliceChecksum],
484) -> bool {
485 debug_assert!(chunk_offset.is_multiple_of(slice_size));
486 let slice = slice_size as usize;
487 let first_slice = (chunk_offset / slice_size) as usize;
488 for (index, window) in data.chunks(slice).enumerate() {
489 let Some(expected) = checksums.get(first_slice + index) else {
490 return false;
491 };
492 let window_end = chunk_offset + (index as u64) * slice_size + window.len() as u64;
493 if window.len() != slice && window_end != file_len {
494 return false;
495 }
496 if checksum::crc32_padded(window, slice_size) != expected.crc32 {
497 return true;
498 }
499 }
500 false
501}
502
503fn verify_full_hash_streaming(
504 expected_hash: [u8; 16],
505 actual_len: u64,
506 file_id: &FileId,
507 access: &dyn FileAccess,
508) -> Option<bool> {
509 let mut state = checksum::FileHashState::new();
510 let mut buf = vec![0u8; VERIFY_FULL_HASH_CHUNK_BYTES];
511
512 if let Some(mut reader) = access.open_sequential_reader(file_id).ok()? {
513 let mut total_read = 0u64;
514 loop {
515 let read_len = reader.read(&mut buf).ok()?;
516 if read_len == 0 {
517 break;
518 }
519 state.update(&buf[..read_len]);
520 total_read += read_len as u64;
521 }
522 if total_read != actual_len {
523 return Some(false);
524 }
525 } else {
526 let mut offset = 0u64;
527 while offset < actual_len {
528 let chunk_len = ((actual_len - offset) as usize).min(buf.len());
529 let read_len = access
530 .read_file_range_into(file_id, offset, &mut buf[..chunk_len])
531 .ok()?;
532 if read_len == 0 {
533 return Some(false);
534 }
535 state.update(&buf[..read_len]);
536 offset += read_len as u64;
537 }
538 }
539
540 Some(state.finalize() == expected_hash)
541}
542
543fn update_quick_and_full_hash_states(
544 quick_state: &mut checksum::FileHashState,
545 full_state: &mut Option<checksum::FileHashState>,
546 data: &[u8],
547) {
548 if data.is_empty() {
549 return;
550 }
551 if let Some(full_state) = full_state.as_mut() {
552 full_state.update(data);
553 return;
554 }
555
556 let quick_remaining = QUICK_CHECK_16K_BYTES.saturating_sub(quick_state.bytes_fed() as usize);
557 if data.len() <= quick_remaining {
558 quick_state.update(data);
559 return;
560 }
561
562 quick_state.update(&data[..quick_remaining]);
563 let mut cloned = quick_state.clone();
564 cloned.update(&data[quick_remaining..]);
565 *full_state = Some(cloned);
566}
567
568pub fn verify_slices(
572 par2: &Par2FileSet,
573 file_id: &FileId,
574 access: &dyn FileAccess,
575) -> Option<Vec<bool>> {
576 let desc = par2.file_description(file_id)?;
577 let checksums = par2.file_checksums(file_id)?;
578 let slice_size = par2.slice_size;
579 let expected_slices = bounded_slice_count(par2, desc.length)?;
580 if checksums.len() != expected_slices {
581 return Some(vec![false; expected_slices]);
582 }
583
584 if !access.file_exists(file_id) {
585 return Some(vec![false; expected_slices]);
586 }
587
588 if let Some(results) =
589 verify_slices_batched_md5(file_id, desc.length, checksums, slice_size, access).ok()?
590 {
591 return Some(results);
592 }
593
594 let mut results = vec![false; expected_slices];
595 let mut buf = vec![0u8; VERIFY_SLICE_CHUNK_BYTES];
596
597 for (i, result) in results.iter_mut().enumerate() {
598 let offset = i as u64 * slice_size;
599 let expected_data_len = desc.length.saturating_sub(offset).min(slice_size);
600 let actual = checksum_file_slice_padded(
601 file_id,
602 offset,
603 expected_data_len,
604 slice_size,
605 access,
606 &mut buf,
607 )
608 .ok()?;
609 *result = actual.crc32 == checksums[i].crc32 && actual.md5 == checksums[i].md5;
610 }
611
612 Some(results)
613}
614
615fn verify_slices_batched_md5(
616 file_id: &FileId,
617 file_len: u64,
618 checksums: &[SliceChecksum],
619 slice_size: u64,
620 access: &dyn FileAccess,
621) -> io::Result<Option<Vec<bool>>> {
622 if checksums.is_empty() {
623 return Ok(Some(Vec::new()));
624 }
625 let Ok(slice_size_usize) = usize::try_from(slice_size) else {
626 return Ok(None);
627 };
628 if slice_size_usize == 0 {
629 return Ok(None);
630 }
631
632 let max_lanes = (VERIFY_SIMD_BATCH_MEMORY_BYTES / slice_size_usize).min(md5_simd::max_lanes());
636 debug_assert!(max_lanes <= VERIFY_SIMD_MAX_LANES);
637 if max_lanes < 2 {
638 return Ok(None);
639 }
640
641 let mut results = vec![false; checksums.len()];
642 let mut buffers = (0..max_lanes)
643 .map(|_| vec![0u8; slice_size_usize])
644 .collect::<Vec<_>>();
645
646 if let Some(mut reader) = access.open_sequential_reader(file_id)? {
647 return verify_slices_batched_md5_from_reader(
648 &mut *reader,
649 file_len,
650 checksums,
651 slice_size,
652 &mut buffers,
653 )
654 .map(Some);
655 }
656
657 let mut index = 0usize;
658
659 while index < checksums.len() {
660 let lanes = max_lanes.min(checksums.len() - index);
661 let mut read_lens = Vec::with_capacity(lanes);
662 let mut crc32s = Vec::with_capacity(lanes);
663
664 for (lane, buffer) in buffers.iter_mut().take(lanes).enumerate() {
665 let slice_index = index + lane;
666 let offset = slice_index as u64 * slice_size;
667 let expected_data_len = file_len.saturating_sub(offset).min(slice_size);
668 let read_len =
669 read_file_slice_into(file_id, offset, expected_data_len, access, buffer)?;
670 read_lens.push(read_len);
671 crc32s.push(checksum::crc32_padded(&buffer[..read_len], slice_size));
672 }
673
674 let inputs = buffers
675 .iter()
676 .take(lanes)
677 .zip(read_lens.iter())
678 .map(|(buffer, read_len)| &buffer[..*read_len])
679 .collect::<Vec<_>>();
680 let md5s = md5_simd::md5_multi(&inputs, Some(slice_size));
681
682 for lane in 0..lanes {
683 let expected = checksums[index + lane];
684 results[index + lane] = crc32s[lane] == expected.crc32 && md5s[lane] == expected.md5;
685 }
686
687 index += lanes;
688 }
689
690 Ok(Some(results))
691}
692
693fn verify_slices_batched_md5_from_reader(
694 reader: &mut dyn Read,
695 file_len: u64,
696 checksums: &[SliceChecksum],
697 slice_size: u64,
698 buffers: &mut [Vec<u8>],
699) -> io::Result<Vec<bool>> {
700 let mut results = vec![false; checksums.len()];
701 let max_lanes = buffers.len();
702 let mut index = 0usize;
703
704 while index < checksums.len() {
705 let lanes = max_lanes.min(checksums.len() - index);
706 let mut read_lens = Vec::with_capacity(lanes);
707 let mut crc32s = Vec::with_capacity(lanes);
708
709 for (lane, buffer) in buffers.iter_mut().take(lanes).enumerate() {
710 let slice_index = index + lane;
711 let offset = slice_index as u64 * slice_size;
712 let expected_data_len = file_len.saturating_sub(offset).min(slice_size) as usize;
713 let read_len = read_from_sequential_reader(reader, buffer, expected_data_len)?;
714 read_lens.push(read_len);
715 crc32s.push(checksum::crc32_padded(&buffer[..read_len], slice_size));
716 }
717
718 let inputs = buffers
719 .iter()
720 .take(lanes)
721 .zip(read_lens.iter())
722 .map(|(buffer, read_len)| &buffer[..*read_len])
723 .collect::<Vec<_>>();
724 let md5s = md5_simd::md5_multi(&inputs, Some(slice_size));
725
726 for lane in 0..lanes {
727 let expected = checksums[index + lane];
728 results[index + lane] = crc32s[lane] == expected.crc32 && md5s[lane] == expected.md5;
729 }
730
731 index += lanes;
732 }
733
734 Ok(results)
735}
736
737fn read_from_sequential_reader(
738 reader: &mut dyn Read,
739 dst: &mut [u8],
740 expected_len: usize,
741) -> io::Result<usize> {
742 let expected_len = expected_len.min(dst.len());
743 let mut read_len = 0usize;
744 while read_len < expected_len {
745 let n = reader.read(&mut dst[read_len..expected_len])?;
746 if n == 0 {
747 break;
748 }
749 read_len += n;
750 }
751 Ok(read_len)
752}
753
754fn read_file_slice_into(
755 file_id: &FileId,
756 offset: u64,
757 expected_data_len: u64,
758 access: &dyn FileAccess,
759 dst: &mut [u8],
760) -> io::Result<usize> {
761 let mut consumed = 0u64;
762
763 while consumed < expected_data_len {
764 let start = consumed as usize;
765 let remaining_capacity = dst.len().saturating_sub(start);
766 if remaining_capacity == 0 {
767 break;
768 }
769 let take = (expected_data_len - consumed).min(remaining_capacity as u64) as usize;
770 let read_len = access.read_file_range_into(
771 file_id,
772 offset + consumed,
773 &mut dst[start..start + take],
774 )?;
775 if read_len == 0 {
776 break;
777 }
778 consumed += read_len as u64;
779 if read_len < take {
780 break;
781 }
782 }
783
784 Ok(consumed as usize)
785}
786
787fn checksum_file_slice_padded(
788 file_id: &FileId,
789 offset: u64,
790 expected_data_len: u64,
791 slice_size: u64,
792 access: &dyn FileAccess,
793 buf: &mut [u8],
794) -> io::Result<SliceChecksum> {
795 let mut state = checksum::SliceChecksumState::new();
796 let mut consumed = 0u64;
797
798 while consumed < expected_data_len {
799 let take = (expected_data_len - consumed).min(buf.len() as u64) as usize;
800 let read_len = access.read_file_range_into(file_id, offset + consumed, &mut buf[..take])?;
801 if read_len == 0 {
802 break;
803 }
804 state.update(&buf[..read_len]);
805 consumed += read_len as u64;
806 if read_len < take {
807 break;
808 }
809 }
810
811 let (crc32, md5) = state.finalize(Some(slice_size));
812 Ok(SliceChecksum { crc32, md5 })
813}
814
815pub fn verify_slices_from_crcs(
823 par2: &Par2FileSet,
824 file_id: &FileId,
825 slice_crcs: &[u32],
826) -> Option<Vec<bool>> {
827 let checksums = par2.file_checksums(file_id)?;
828
829 let results: Vec<bool> = checksums
830 .iter()
831 .enumerate()
832 .map(|(i, expected)| {
833 slice_crcs
834 .get(i)
835 .map(|&crc| crc == expected.crc32)
836 .unwrap_or(false)
837 })
838 .collect();
839
840 Some(results)
841}
842
843#[derive(Default)]
845#[non_exhaustive]
848pub struct VerifyOptions {
849 pub cancel: Option<CancellationToken>,
851 pub progress: Option<ProgressCallback>,
853 pub proven_slices: HashMap<FileId, Vec<bool>>,
893 pub fast_verify: bool,
902}
903
904impl VerifyOptions {
905 fn proven_slices_for(&self, file_id: &FileId, slice_count: usize) -> Option<&[bool]> {
916 if slice_count == 0 {
917 return None;
918 }
919 let proven = self.proven_slices.get(file_id)?;
920 if proven.len() != slice_count {
921 return None;
922 }
923 proven
924 .iter()
925 .any(|proven| *proven)
926 .then_some(proven.as_slice())
927 }
928}
929
930fn proven_slices_for_file<'a>(
935 par2: &Par2FileSet,
936 options: &'a VerifyOptions,
937 file_id: &FileId,
938) -> Option<&'a [bool]> {
939 let desc = par2.file_description(file_id)?;
940 let slice_count = bounded_slice_count(par2, desc.length)?;
941 options.proven_slices_for(file_id, slice_count)
942}
943
944fn fast_verify_enabled(flag: bool) -> bool {
950 resolve_fast_verify(
951 std::env::var("WEAVER_PAR2_FAST_VERIFY").ok().as_deref(),
952 flag,
953 )
954}
955
956fn resolve_fast_verify(env_value: Option<&str>, flag: bool) -> bool {
960 match env_value {
961 Some("1") => true,
962 Some("0") => false,
963 _ => flag,
964 }
965}
966
967pub fn verify_all(par2: &Par2FileSet, access: &dyn FileAccess) -> VerificationResult {
969 verify_all_with_options(par2, access, &VerifyOptions::default())
970}
971
972pub fn verify_selected_file_ids(
974 par2: &Par2FileSet,
975 access: &dyn FileAccess,
976 file_ids: &[FileId],
977) -> VerificationResult {
978 verify_selected_file_ids_with_options(par2, access, file_ids, &VerifyOptions::default())
979}
980
981pub fn verify_all_with_options(
983 par2: &Par2FileSet,
984 access: &dyn FileAccess,
985 options: &VerifyOptions,
986) -> VerificationResult {
987 verify_selected_file_ids_with_options(par2, access, &par2.recovery_file_ids, options)
988}
989
990pub fn verify_selected_file_ids_parallel(
996 par2: &Par2FileSet,
997 access: &(dyn FileAccess + Sync),
998 file_ids: &[FileId],
999) -> VerificationResult {
1000 verify_selected_file_ids_parallel_with_options(
1001 par2,
1002 access,
1003 file_ids,
1004 &VerifyOptions::default(),
1005 )
1006}
1007
1008pub fn verify_selected_file_ids_parallel_with_options(
1021 par2: &Par2FileSet,
1022 access: &(dyn FileAccess + Sync),
1023 file_ids: &[FileId],
1024 options: &VerifyOptions,
1025) -> VerificationResult {
1026 use rayon::prelude::*;
1027 let fast_verify = fast_verify_enabled(options.fast_verify);
1028 let span_parallel = file_ids.len() == 1;
1032 let partials: Vec<VerificationResult> = file_ids
1033 .par_iter()
1034 .map(|file_id| {
1035 if let Some(proven) = proven_slices_for_file(par2, options, file_id)
1038 && let Some(file) = verify_file_sliced_with_evidence(
1039 par2,
1040 access,
1041 file_id,
1042 span_parallel,
1043 Some(proven),
1044 )
1045 {
1046 return single_file_result(par2, file);
1047 }
1048 if fast_verify
1049 && let Some(file) = verify_file_sliced(par2, access, file_id, span_parallel)
1050 {
1051 return single_file_result(par2, file);
1052 }
1053 verify_selected_file_ids_resolved(
1060 par2,
1061 access,
1062 span_parallel.then_some(access),
1063 std::slice::from_ref(file_id),
1064 &VerifyOptions::default(),
1065 fast_verify,
1066 )
1067 })
1068 .collect();
1069 combine_partial_results(par2, partials)
1070}
1071
1072pub fn verify_repaired_file_ids_parallel(
1085 par2: &Par2FileSet,
1086 access: &(dyn FileAccess + Sync),
1087 file_ids: &[FileId],
1088) -> VerificationResult {
1089 use rayon::prelude::*;
1090 let span_parallel = file_ids.len() == 1;
1096 let partials: Vec<VerificationResult> = file_ids
1097 .par_iter()
1098 .map(|file_id| {
1099 verify_repaired_file_sliced(par2, access, file_id, span_parallel).unwrap_or_else(|| {
1100 verify_selected_file_ids(par2, access, std::slice::from_ref(file_id))
1101 })
1102 })
1103 .collect();
1104 combine_partial_results(par2, partials)
1105}
1106
1107struct SlicedVerifyPlan {
1112 filename: String,
1113 length: u64,
1114 slice_size: u64,
1115 slice_count: usize,
1116 work: SlicedVerifyWork,
1117 proven: Option<Vec<bool>>,
1121}
1122
1123enum SlicedVerifyWork {
1143 Spans(Vec<(usize, usize)>),
1145 Slices(Vec<usize>),
1148}
1149
1150fn sliced_verify_plan(
1169 par2: &Par2FileSet,
1170 access: &dyn FileAccess,
1171 file_id: &FileId,
1172 proven: Option<&[bool]>,
1173) -> Option<SlicedVerifyPlan> {
1174 let desc = par2.file_description(file_id)?;
1175 let checksums = par2.file_checksums(file_id)?;
1176 let slice_size = par2.slice_size;
1177 if desc.length == 0 || slice_size == 0 {
1178 return None;
1179 }
1180 let expected_slices = bounded_slice_count(par2, desc.length)?;
1181 if expected_slices == 0 || checksums.len() != expected_slices {
1182 return None;
1183 }
1184 if access.file_length(file_id) != Some(desc.length) {
1185 return None;
1186 }
1187 let proven = proven.filter(|proven| proven.len() == expected_slices);
1188
1189 let work = match proven {
1190 None => {
1191 let span_slices = ((VERIFY_SPAN_TARGET_BYTES as u64 / slice_size).max(1) as usize)
1192 .min(expected_slices);
1193 SlicedVerifyWork::Spans(
1194 (0..expected_slices)
1195 .step_by(span_slices)
1196 .map(|start| (start, span_slices.min(expected_slices - start)))
1197 .collect(),
1198 )
1199 }
1200 Some(proven) => SlicedVerifyWork::Slices(
1201 proven
1202 .iter()
1203 .enumerate()
1204 .filter_map(|(index, proven)| (!*proven).then_some(index))
1205 .collect(),
1206 ),
1207 };
1208 Some(SlicedVerifyPlan {
1209 filename: desc.filename.clone(),
1210 length: desc.length,
1211 slice_size,
1212 slice_count: expected_slices,
1213 work,
1214 proven: proven.map(<[bool]>::to_vec),
1215 })
1216}
1217
1218fn slice_batch_lanes(slice_size: u64) -> usize {
1224 let Ok(slice_size) = usize::try_from(slice_size) else {
1225 return 1;
1226 };
1227 if slice_size == 0 {
1228 return 1;
1229 }
1230 (VERIFY_SIMD_BATCH_MEMORY_BYTES / slice_size)
1231 .min(md5_simd::max_lanes())
1232 .clamp(1, VERIFY_SIMD_MAX_LANES)
1233}
1234
1235fn slice_task_slices(slice_size: u64, lanes: usize) -> usize {
1239 let per_task = (VERIFY_SPAN_TARGET_BYTES as u64 / slice_size.max(1)).max(1) as usize;
1240 per_task.div_ceil(lanes).max(1) * lanes
1241}
1242
1243fn assemble_valid_slices(plan: &SlicedVerifyPlan, results: Vec<Vec<bool>>) -> Vec<bool> {
1251 let mut valid = match &plan.proven {
1252 Some(proven) => proven.clone(),
1253 None => vec![false; plan.slice_count],
1254 };
1255 match &plan.work {
1256 SlicedVerifyWork::Spans(spans) => {
1257 for (&(start, count), results) in spans.iter().zip(results) {
1258 debug_assert_eq!(results.len(), count, "a span reports one verdict per slice");
1259 let end = (start + count).min(valid.len());
1260 valid[start..end].copy_from_slice(&results[..end - start]);
1261 }
1262 }
1263 SlicedVerifyWork::Slices(indices) => {
1264 for (index, result) in indices.iter().zip(results.into_iter().flatten()) {
1265 valid[*index] = result;
1266 }
1267 }
1268 }
1269 valid
1270}
1271
1272fn check_slice_batch(
1285 access: &dyn FileAccess,
1286 file_id: &FileId,
1287 checksums: &[SliceChecksum],
1288 plan: &SlicedVerifyPlan,
1289 indices: &[usize],
1290 buffers: &mut Vec<Vec<u8>>,
1291) -> Vec<bool> {
1292 let slice_size = plan.slice_size;
1293 let Ok(slice_len) = usize::try_from(slice_size) else {
1294 return vec![false; indices.len()];
1295 };
1296 while buffers.len() < indices.len() {
1297 buffers.push(vec![0u8; slice_len]);
1298 }
1299
1300 let mut read_lens = Vec::with_capacity(indices.len());
1301 let mut read_complete = Vec::with_capacity(indices.len());
1302 for (lane, &index) in indices.iter().enumerate() {
1303 let offset = index as u64 * slice_size;
1304 let want = plan.length.saturating_sub(offset).min(slice_size);
1305 let buffer = &mut buffers[lane];
1306 if buffer.len() < slice_len {
1307 buffer.resize(slice_len, 0);
1308 }
1309 match read_file_slice_into(file_id, offset, want, access, &mut buffer[..slice_len]) {
1310 Ok(read) => {
1311 read_lens.push(read);
1312 read_complete.push(read as u64 == want);
1313 }
1314 Err(_) => {
1315 read_lens.push(0);
1316 read_complete.push(false);
1317 }
1318 }
1319 }
1320
1321 let inputs: Vec<&[u8]> = buffers
1322 .iter()
1323 .zip(read_lens.iter())
1324 .map(|(buffer, read)| &buffer[..*read])
1325 .collect();
1326 let md5s = md5_simd::md5_multi(&inputs, Some(slice_size));
1327 indices
1328 .iter()
1329 .enumerate()
1330 .map(|(lane, index)| {
1331 let expected = &checksums[*index];
1332 read_complete[lane]
1333 && checksum::crc32_padded(inputs[lane], slice_size) == expected.crc32
1334 && md5s[lane] == expected.md5
1335 })
1336 .collect()
1337}
1338
1339fn check_slice_indices(
1341 access: &dyn FileAccess,
1342 file_id: &FileId,
1343 checksums: &[SliceChecksum],
1344 plan: &SlicedVerifyPlan,
1345 indices: &[usize],
1346 buffers: &mut Vec<Vec<u8>>,
1347) -> Vec<bool> {
1348 let lanes = slice_batch_lanes(plan.slice_size);
1349 indices
1350 .chunks(lanes)
1351 .flat_map(|batch| check_slice_batch(access, file_id, checksums, plan, batch, buffers))
1352 .collect()
1353}
1354
1355fn check_slice_span(
1372 access: &dyn FileAccess,
1373 file_id: &FileId,
1374 checksums: &[SliceChecksum],
1375 plan: &SlicedVerifyPlan,
1376 start: usize,
1377 count: usize,
1378 scratch: &mut Vec<u8>,
1379) -> Vec<bool> {
1380 let slice_size = plan.slice_size;
1381 let offset = start as u64 * slice_size;
1382 let want = (plan.length - offset).min(count as u64 * slice_size);
1383 let Ok(want_len) = usize::try_from(want) else {
1384 return vec![false; count];
1385 };
1386 if scratch.len() < want_len {
1387 scratch.resize(want_len, 0);
1388 }
1389 let Ok(read) = read_file_slice_into(file_id, offset, want, access, &mut scratch[..want_len])
1390 else {
1391 return vec![false; count];
1392 };
1393 if read != want_len {
1394 return vec![false; count];
1395 }
1396 let data = &scratch[..want_len];
1397
1398 let mut valid = Vec::with_capacity(count);
1399 let mut index = 0usize;
1400 let kernel_lanes = md5_simd::max_lanes().min(VERIFY_SIMD_MAX_LANES);
1401 while index < count {
1402 let lanes = kernel_lanes.min(count - index);
1403 let mut inputs: [&[u8]; VERIFY_SIMD_MAX_LANES] = [&[]; VERIFY_SIMD_MAX_LANES];
1404 for (lane, input) in inputs.iter_mut().take(lanes).enumerate() {
1405 let slice_index = (index + lane) as u64;
1406 let lo = (slice_index * slice_size).min(want) as usize;
1407 let hi = ((slice_index + 1) * slice_size).min(want) as usize;
1408 *input = &data[lo..hi];
1409 }
1410 let inputs = &inputs[..lanes];
1411 let md5s = md5_simd::md5_multi(inputs, Some(slice_size));
1412 for (lane, input) in inputs.iter().enumerate() {
1413 let expected = &checksums[start + index + lane];
1414 let crc32 = checksum::crc32_padded(input, slice_size);
1415 valid.push(crc32 == expected.crc32 && md5s[lane] == expected.md5);
1416 }
1417 index += lanes;
1418 }
1419 valid
1420}
1421
1422fn finish_sliced_verification(
1427 file_id: &FileId,
1428 plan: &SlicedVerifyPlan,
1429 valid_slices: Vec<bool>,
1430) -> FileVerification {
1431 let damaged = valid_slices.iter().filter(|valid| !**valid).count() as u32;
1432 let status = if damaged == 0 {
1433 FileStatus::Complete
1434 } else {
1435 FileStatus::Damaged(damaged)
1436 };
1437 FileVerification {
1438 file_id: *file_id,
1439 filename: plan.filename.clone(),
1440 status,
1441 valid_slices,
1442 missing_slice_count: damaged,
1443 }
1444}
1445
1446fn strict_slice_validity(
1455 par2: &Par2FileSet,
1456 access: &dyn FileAccess,
1457 span_access: Option<&(dyn FileAccess + Sync)>,
1458 file_id: &FileId,
1459 slice_count: usize,
1460) -> Vec<bool> {
1461 if let Some(span_access) = span_access
1462 && let Some(file) = verify_file_sliced(par2, span_access, file_id, true)
1463 && file.valid_slices.len() == slice_count
1464 {
1465 return file.valid_slices;
1466 }
1467 verify_slices(par2, file_id, access).unwrap_or_else(|| vec![false; slice_count])
1468}
1469
1470fn single_file_result(par2: &Par2FileSet, file: FileVerification) -> VerificationResult {
1474 let damaged = file.missing_slice_count;
1475 let files = vec![file];
1476 let recovery_blocks_available = par2.recovery_block_count();
1477 let repairable = repairability_for_result_with_resource_limit(
1478 &files,
1479 damaged,
1480 recovery_blocks_available,
1481 None,
1482 );
1483 VerificationResult {
1484 files,
1485 recovery_blocks_available,
1486 total_missing_blocks: damaged,
1487 repairable,
1488 }
1489}
1490
1491fn verify_file_sliced(
1511 par2: &Par2FileSet,
1512 access: &(dyn FileAccess + Sync),
1513 file_id: &FileId,
1514 span_parallel: bool,
1515) -> Option<FileVerification> {
1516 verify_file_sliced_with_evidence(par2, access, file_id, span_parallel, None)
1517}
1518
1519fn verify_file_sliced_with_evidence(
1529 par2: &Par2FileSet,
1530 access: &(dyn FileAccess + Sync),
1531 file_id: &FileId,
1532 span_parallel: bool,
1533 proven: Option<&[bool]>,
1534) -> Option<FileVerification> {
1535 use rayon::prelude::*;
1536 let plan = sliced_verify_plan(par2, access, file_id, proven)?;
1537 let checksums = par2.file_checksums(file_id)?;
1538 let results: Vec<Vec<bool>> = match (&plan.work, span_parallel) {
1539 (SlicedVerifyWork::Spans(spans), true) => spans
1542 .par_iter()
1543 .map_init(Vec::new, |scratch, &(start, count)| {
1544 check_slice_span(access, file_id, checksums, &plan, start, count, scratch)
1545 })
1546 .collect(),
1547 (SlicedVerifyWork::Spans(spans), false) => {
1548 let mut scratch = Vec::new();
1549 spans
1550 .iter()
1551 .map(|&(start, count)| {
1552 check_slice_span(
1553 access,
1554 file_id,
1555 checksums,
1556 &plan,
1557 start,
1558 count,
1559 &mut scratch,
1560 )
1561 })
1562 .collect()
1563 }
1564 (SlicedVerifyWork::Slices(indices), true) => {
1565 let lanes = slice_batch_lanes(plan.slice_size);
1566 indices
1567 .par_chunks(slice_task_slices(plan.slice_size, lanes))
1568 .map_init(Vec::new, |buffers, chunk| {
1569 check_slice_indices(access, file_id, checksums, &plan, chunk, buffers)
1570 })
1571 .collect()
1572 }
1573 (SlicedVerifyWork::Slices(indices), false) => {
1574 let mut buffers = Vec::new();
1575 vec![check_slice_indices(
1576 access,
1577 file_id,
1578 checksums,
1579 &plan,
1580 indices,
1581 &mut buffers,
1582 )]
1583 }
1584 };
1585 let valid_slices = assemble_valid_slices(&plan, results);
1586 Some(finish_sliced_verification(file_id, &plan, valid_slices))
1587}
1588
1589fn verify_file_sliced_serial(
1595 par2: &Par2FileSet,
1596 access: &dyn FileAccess,
1597 file_id: &FileId,
1598) -> Option<FileVerification> {
1599 verify_file_sliced_serial_with_evidence(par2, access, file_id, None)
1600}
1601
1602fn verify_file_sliced_serial_with_evidence(
1607 par2: &Par2FileSet,
1608 access: &dyn FileAccess,
1609 file_id: &FileId,
1610 proven: Option<&[bool]>,
1611) -> Option<FileVerification> {
1612 let plan = sliced_verify_plan(par2, access, file_id, proven)?;
1613 let checksums = par2.file_checksums(file_id)?;
1614 let results: Vec<Vec<bool>> = match &plan.work {
1615 SlicedVerifyWork::Spans(spans) => {
1616 let mut scratch = Vec::new();
1617 spans
1618 .iter()
1619 .map(|&(start, count)| {
1620 check_slice_span(
1621 access,
1622 file_id,
1623 checksums,
1624 &plan,
1625 start,
1626 count,
1627 &mut scratch,
1628 )
1629 })
1630 .collect()
1631 }
1632 SlicedVerifyWork::Slices(indices) => {
1633 let mut buffers = Vec::new();
1634 vec![check_slice_indices(
1635 access,
1636 file_id,
1637 checksums,
1638 &plan,
1639 indices,
1640 &mut buffers,
1641 )]
1642 }
1643 };
1644 let valid_slices = assemble_valid_slices(&plan, results);
1645 Some(finish_sliced_verification(file_id, &plan, valid_slices))
1646}
1647
1648fn verify_repaired_file_sliced(
1656 par2: &Par2FileSet,
1657 access: &(dyn FileAccess + Sync),
1658 file_id: &FileId,
1659 span_parallel: bool,
1660) -> Option<VerificationResult> {
1661 verify_file_sliced(par2, access, file_id, span_parallel)
1662 .map(|file| single_file_result(par2, file))
1663}
1664
1665fn combine_partial_results(
1668 par2: &Par2FileSet,
1669 partials: Vec<VerificationResult>,
1670) -> VerificationResult {
1671 let mut files = Vec::with_capacity(partials.len());
1672 let mut total_missing_blocks = 0u32;
1673 let mut resource_limit_reason = None;
1674 for partial in partials {
1675 total_missing_blocks = total_missing_blocks.saturating_add(partial.total_missing_blocks);
1676 if let Repairability::ResourceLimited { reason } = &partial.repairable {
1677 resource_limit_reason.get_or_insert_with(|| reason.clone());
1678 }
1679 files.extend(partial.files);
1680 }
1681
1682 let recovery_blocks_available = par2.recovery_block_count();
1683 let repairable = repairability_for_result_with_resource_limit(
1684 &files,
1685 total_missing_blocks,
1686 recovery_blocks_available,
1687 resource_limit_reason,
1688 );
1689 VerificationResult {
1690 files,
1691 recovery_blocks_available,
1692 total_missing_blocks,
1693 repairable,
1694 }
1695}
1696
1697pub fn merge_verification_results(
1702 par2: &Par2FileSet,
1703 base: &VerificationResult,
1704 updated: VerificationResult,
1705) -> VerificationResult {
1706 let mut updated_by_id: HashMap<FileId, FileVerification> = updated
1707 .files
1708 .into_iter()
1709 .map(|file| (file.file_id, file))
1710 .collect();
1711 let files: Vec<FileVerification> = base
1712 .files
1713 .iter()
1714 .map(|file| {
1715 updated_by_id
1716 .remove(&file.file_id)
1717 .unwrap_or_else(|| file.clone())
1718 })
1719 .collect();
1720
1721 let mut total_missing_blocks = 0u32;
1722 for file in &files {
1723 total_missing_blocks = total_missing_blocks.saturating_add(file.missing_slice_count);
1724 }
1725 let resource_limit_reason = match &updated.repairable {
1726 Repairability::ResourceLimited { reason } => Some(reason.clone()),
1727 _ => match &base.repairable {
1728 Repairability::ResourceLimited { reason } => Some(reason.clone()),
1729 _ => None,
1730 },
1731 };
1732
1733 let recovery_blocks_available = par2.recovery_block_count();
1734 let repairable = repairability_for_result_with_resource_limit(
1735 &files,
1736 total_missing_blocks,
1737 recovery_blocks_available,
1738 resource_limit_reason,
1739 );
1740 VerificationResult {
1741 files,
1742 recovery_blocks_available,
1743 total_missing_blocks,
1744 repairable,
1745 }
1746}
1747
1748pub fn verify_selected_file_ids_with_options(
1750 par2: &Par2FileSet,
1751 access: &dyn FileAccess,
1752 file_ids: &[FileId],
1753 options: &VerifyOptions,
1754) -> VerificationResult {
1755 let fast_verify = fast_verify_enabled(options.fast_verify);
1757 verify_selected_file_ids_resolved(par2, access, None, file_ids, options, fast_verify)
1758}
1759
1760fn verify_selected_file_ids_resolved(
1771 par2: &Par2FileSet,
1772 access: &dyn FileAccess,
1773 span_access: Option<&(dyn FileAccess + Sync)>,
1774 file_ids: &[FileId],
1775 options: &VerifyOptions,
1776 fast_verify: bool,
1777) -> VerificationResult {
1778 let mut files = Vec::new();
1779 let mut total_missing_blocks = 0u32;
1780 let mut resource_limit_reason = None;
1781 let total_files = file_ids.len() as u32;
1782 let mut bytes_processed = 0u64;
1783
1784 for (file_index, file_id) in file_ids.iter().enumerate() {
1785 if let Some(ref cancel) = options.cancel
1787 && cancel.is_cancelled()
1788 {
1789 break;
1790 }
1791
1792 let desc = match par2.file_description(file_id) {
1793 Some(d) => d,
1794 None => continue,
1795 };
1796 let Some(slice_count) = bounded_slice_count(par2, desc.length) else {
1797 resource_limit_reason.get_or_insert_with(|| {
1798 format!("file {} exceeds verifier slice limits", desc.filename)
1799 });
1800 files.push(resource_limited_verification(
1801 *file_id,
1802 desc.filename.clone(),
1803 ));
1804 continue;
1805 };
1806 let slice_count_u32 = slice_count as u32;
1807
1808 if !access.file_exists(file_id) {
1809 total_missing_blocks = total_missing_blocks.saturating_add(slice_count_u32);
1810 files.push(FileVerification {
1811 file_id: *file_id,
1812 filename: desc.filename.clone(),
1813 status: FileStatus::Missing,
1814 valid_slices: vec![false; slice_count],
1815 missing_slice_count: slice_count_u32,
1816 });
1817 continue;
1818 }
1819
1820 let Some(actual_len) = access.file_length(file_id) else {
1821 total_missing_blocks = total_missing_blocks.saturating_add(slice_count_u32);
1822 files.push(FileVerification {
1823 file_id: *file_id,
1824 filename: desc.filename.clone(),
1825 status: FileStatus::Damaged(slice_count_u32),
1826 valid_slices: vec![false; slice_count],
1827 missing_slice_count: slice_count_u32,
1828 });
1829 continue;
1830 };
1831
1832 if desc.length == 0 {
1835 let status = if actual_len == 0 {
1836 FileStatus::Complete
1837 } else {
1838 FileStatus::Damaged(0)
1839 };
1840 files.push(FileVerification {
1841 file_id: *file_id,
1842 filename: desc.filename.clone(),
1843 status,
1844 valid_slices: vec![],
1845 missing_slice_count: 0,
1846 });
1847 continue;
1848 }
1849
1850 if actual_len == 0 && desc.length > 0 {
1852 total_missing_blocks = total_missing_blocks.saturating_add(slice_count_u32);
1853 files.push(FileVerification {
1854 file_id: *file_id,
1855 filename: desc.filename.clone(),
1856 status: FileStatus::Damaged(slice_count_u32),
1857 valid_slices: vec![false; slice_count],
1858 missing_slice_count: slice_count_u32,
1859 });
1860 continue;
1861 }
1862
1863 if par2.file_checksums(file_id).is_none() {
1866 let full_ok = verify_full_hash(par2, file_id, access).unwrap_or(false);
1867 if full_ok {
1868 files.push(FileVerification {
1869 file_id: *file_id,
1870 filename: desc.filename.clone(),
1871 status: FileStatus::Complete,
1872 valid_slices: vec![true; slice_count],
1873 missing_slice_count: 0,
1874 });
1875 } else {
1876 total_missing_blocks = total_missing_blocks.saturating_add(slice_count_u32);
1879 files.push(FileVerification {
1880 file_id: *file_id,
1881 filename: desc.filename.clone(),
1882 status: FileStatus::Damaged(slice_count_u32),
1883 valid_slices: vec![false; slice_count],
1884 missing_slice_count: slice_count_u32,
1885 });
1886 }
1887 bytes_processed += desc.length;
1888 if let Some(ref progress) = options.progress {
1889 progress(ProgressUpdate {
1890 stage: ProgressStage::Verifying,
1891 current: file_index as u32 + 1,
1892 total: total_files,
1893 bytes_processed,
1894 total_bytes: None,
1895 phase: ProgressPhase::Whole,
1896 });
1897 }
1898 continue;
1899 }
1900
1901 if actual_len != desc.length {
1902 let valid = strict_slice_validity(par2, access, span_access, file_id, slice_count);
1903 let damaged = valid.iter().filter(|&&v| !v).count() as u32;
1904 total_missing_blocks = total_missing_blocks.saturating_add(damaged);
1905 files.push(FileVerification {
1906 file_id: *file_id,
1907 filename: desc.filename.clone(),
1908 status: FileStatus::Damaged(damaged),
1909 valid_slices: valid,
1910 missing_slice_count: damaged,
1911 });
1912 continue;
1913 }
1914
1915 if let Some(proven) = options.proven_slices_for(file_id, slice_count)
1928 && let Some(evidenced) =
1929 verify_file_sliced_serial_with_evidence(par2, access, file_id, Some(proven))
1930 {
1931 total_missing_blocks =
1932 total_missing_blocks.saturating_add(evidenced.missing_slice_count);
1933 files.push(evidenced);
1934 bytes_processed += desc.length;
1939 if let Some(ref progress) = options.progress {
1940 progress(ProgressUpdate {
1941 stage: ProgressStage::Verifying,
1942 current: file_index as u32 + 1,
1943 total: total_files,
1944 bytes_processed,
1945 total_bytes: None,
1946 phase: ProgressPhase::Whole,
1947 });
1948 }
1949 continue;
1950 }
1951
1952 if fast_verify && let Some(fast) = verify_file_sliced_serial(par2, access, file_id) {
1965 total_missing_blocks = total_missing_blocks.saturating_add(fast.missing_slice_count);
1966 files.push(fast);
1967 bytes_processed += desc.length;
1968 if let Some(ref progress) = options.progress {
1969 progress(ProgressUpdate {
1970 stage: ProgressStage::Verifying,
1971 current: file_index as u32 + 1,
1972 total: total_files,
1973 bytes_processed,
1974 total_bytes: None,
1975 phase: ProgressPhase::Whole,
1976 });
1977 }
1978 continue;
1979 }
1980
1981 let checksums = par2
1987 .file_checksums(file_id)
1988 .filter(|checksums| checksums.len() == slice_count);
1989 let outcome = stream_strict_hashes(par2, file_id, access, checksums).unwrap_or(
1990 StrictStreamOutcome::Hashes {
1991 quick_ok: false,
1992 full_ok: false,
1993 },
1994 );
1995
1996 let complete = matches!(
2005 outcome,
2006 StrictStreamOutcome::Hashes {
2007 quick_ok: true,
2008 full_ok: true
2009 }
2010 );
2011 if complete {
2012 files.push(FileVerification {
2013 file_id: *file_id,
2014 filename: desc.filename.clone(),
2015 status: FileStatus::Complete,
2016 valid_slices: vec![true; slice_count],
2017 missing_slice_count: 0,
2018 });
2019 } else {
2020 let valid = strict_slice_validity(par2, access, span_access, file_id, slice_count);
2021 let damaged = valid.iter().filter(|&&v| !v).count() as u32;
2022 total_missing_blocks = total_missing_blocks.saturating_add(damaged);
2023 files.push(FileVerification {
2027 file_id: *file_id,
2028 filename: desc.filename.clone(),
2029 status: FileStatus::Damaged(damaged),
2030 valid_slices: valid,
2031 missing_slice_count: damaged,
2032 });
2033 }
2034
2035 bytes_processed += desc.length;
2036 if let Some(ref progress) = options.progress {
2037 progress(ProgressUpdate {
2038 stage: ProgressStage::Verifying,
2039 current: file_index as u32 + 1,
2040 total: total_files,
2041 bytes_processed,
2042 total_bytes: None,
2043 phase: ProgressPhase::Whole,
2044 });
2045 }
2046 }
2047
2048 let recovery_blocks_available = par2.recovery_block_count();
2049 let repairable = repairability_for_result_with_resource_limit(
2050 &files,
2051 total_missing_blocks,
2052 recovery_blocks_available,
2053 resource_limit_reason,
2054 );
2055
2056 VerificationResult {
2057 files,
2058 recovery_blocks_available,
2059 total_missing_blocks,
2060 repairable,
2061 }
2062}
2063
2064#[cfg(test)]
2065mod tests {
2066 use super::*;
2067 use crate::checksum::{self, SliceChecksumState};
2068 use crate::packet::header;
2069 use crate::par2_set::Par2FileSet;
2070 use crate::types::SliceChecksum;
2071 use md5::{Digest, Md5};
2072 use std::collections::HashMap;
2073 use std::io::{self, Cursor};
2074 use std::sync::{
2075 Arc,
2076 atomic::{AtomicUsize, Ordering},
2077 };
2078
2079 fn make_full_packet(packet_type: &[u8; 16], body: &[u8], recovery_set_id: [u8; 16]) -> Vec<u8> {
2081 let length = (header::HEADER_SIZE + body.len()) as u64;
2082 let mut hash_input = Vec::new();
2083 hash_input.extend_from_slice(&recovery_set_id);
2084 hash_input.extend_from_slice(packet_type);
2085 hash_input.extend_from_slice(body);
2086 let packet_hash: [u8; 16] = Md5::digest(&hash_input).into();
2087
2088 let mut data = Vec::new();
2089 data.extend_from_slice(header::MAGIC);
2090 data.extend_from_slice(&length.to_le_bytes());
2091 data.extend_from_slice(&packet_hash);
2092 data.extend_from_slice(&recovery_set_id);
2093 data.extend_from_slice(packet_type);
2094 data.extend_from_slice(body);
2095 data
2096 }
2097
2098 fn setup_test_set(
2101 file_data: &[u8],
2102 slice_size: u64,
2103 ) -> (Par2FileSet, MemoryFileAccess, FileId) {
2104 setup_test_set_with_full_hash(file_data, slice_size, None)
2105 }
2106
2107 fn setup_test_set_with_full_hash(
2113 file_data: &[u8],
2114 slice_size: u64,
2115 full_hash_override: Option<[u8; 16]>,
2116 ) -> (Par2FileSet, MemoryFileAccess, FileId) {
2117 let file_length = file_data.len() as u64;
2118 let hash_full = full_hash_override.unwrap_or_else(|| checksum::md5(file_data));
2119 let hash_16k_data = &file_data[..file_data.len().min(16384)];
2120 let hash_16k = checksum::md5(hash_16k_data);
2121
2122 let filename = b"testfile.dat";
2124 let mut id_input = Vec::new();
2125 id_input.extend_from_slice(&hash_16k);
2126 id_input.extend_from_slice(&file_length.to_le_bytes());
2127 id_input.extend_from_slice(filename);
2128 let file_id_bytes: [u8; 16] = Md5::digest(&id_input).into();
2129 let file_id = FileId::from_bytes(file_id_bytes);
2130
2131 let num_slices = if file_length == 0 {
2133 0
2134 } else {
2135 file_length.div_ceil(slice_size) as usize
2136 };
2137
2138 let mut checksums = Vec::new();
2139 for i in 0..num_slices {
2140 let offset = i as u64 * slice_size;
2141 let end = ((offset + slice_size) as usize).min(file_data.len());
2142 let slice_data = &file_data[offset as usize..end];
2143
2144 let mut state = SliceChecksumState::new();
2145 state.update(slice_data);
2146 let pad_to = if (slice_data.len() as u64) < slice_size {
2147 Some(slice_size)
2148 } else {
2149 None
2150 };
2151 let (crc, md5) = state.finalize(pad_to);
2152 checksums.push(SliceChecksum { crc32: crc, md5 });
2153 }
2154
2155 let mut main_body = Vec::new();
2157 main_body.extend_from_slice(&slice_size.to_le_bytes());
2158 main_body.extend_from_slice(&1u32.to_le_bytes());
2159 main_body.extend_from_slice(&file_id_bytes);
2160 let rsid: [u8; 16] = Md5::digest(&main_body).into();
2161
2162 let mut fd_body = Vec::new();
2164 fd_body.extend_from_slice(&file_id_bytes);
2165 fd_body.extend_from_slice(&hash_full);
2166 fd_body.extend_from_slice(&hash_16k);
2167 fd_body.extend_from_slice(&file_length.to_le_bytes());
2168 fd_body.extend_from_slice(filename);
2169 while fd_body.len() % 4 != 0 {
2171 fd_body.push(0);
2172 }
2173
2174 let mut ifsc_body = Vec::new();
2176 ifsc_body.extend_from_slice(&file_id_bytes);
2177 for cs in &checksums {
2178 ifsc_body.extend_from_slice(&cs.md5);
2179 ifsc_body.extend_from_slice(&cs.crc32.to_le_bytes());
2180 }
2181
2182 let mut stream = Vec::new();
2184 stream.extend_from_slice(&make_full_packet(header::TYPE_MAIN, &main_body, rsid));
2185 stream.extend_from_slice(&make_full_packet(header::TYPE_FILE_DESC, &fd_body, rsid));
2186 stream.extend_from_slice(&make_full_packet(header::TYPE_IFSC, &ifsc_body, rsid));
2187
2188 let set = Par2FileSet::from_files(&[&stream]).unwrap();
2189
2190 let mut access = MemoryFileAccess::new();
2191 access.add_file(file_id, file_data.to_vec());
2192
2193 (set, access, file_id)
2194 }
2195
2196 fn setup_test_set_multi(
2197 files: &[(&[u8], &str)],
2198 slice_size: u64,
2199 ) -> (Par2FileSet, MemoryFileAccess, Vec<FileId>) {
2200 let mut file_ids = Vec::new();
2201 let mut fd_bodies = Vec::new();
2202 let mut ifsc_bodies = Vec::new();
2203 let mut access = MemoryFileAccess::new();
2204
2205 for &(file_data, filename) in files {
2206 let file_length = file_data.len() as u64;
2207 let hash_full = checksum::md5(file_data);
2208 let hash_16k = checksum::md5(&file_data[..file_data.len().min(16384)]);
2209
2210 let mut id_input = Vec::new();
2211 id_input.extend_from_slice(&hash_16k);
2212 id_input.extend_from_slice(&file_length.to_le_bytes());
2213 id_input.extend_from_slice(filename.as_bytes());
2214 let file_id_bytes: [u8; 16] = Md5::digest(&id_input).into();
2215 let file_id = FileId::from_bytes(file_id_bytes);
2216 file_ids.push(file_id);
2217 access.add_file(file_id, file_data.to_vec());
2218
2219 let num_slices = if file_length == 0 {
2220 0
2221 } else {
2222 file_length.div_ceil(slice_size) as usize
2223 };
2224
2225 let mut checksums = Vec::new();
2226 for i in 0..num_slices {
2227 let offset = i as u64 * slice_size;
2228 let end = ((offset + slice_size) as usize).min(file_data.len());
2229 let slice_data = &file_data[offset as usize..end];
2230
2231 let mut state = SliceChecksumState::new();
2232 state.update(slice_data);
2233 let pad_to = if (slice_data.len() as u64) < slice_size {
2234 Some(slice_size)
2235 } else {
2236 None
2237 };
2238 let (crc, md5) = state.finalize(pad_to);
2239 checksums.push(SliceChecksum { crc32: crc, md5 });
2240 }
2241
2242 let mut fd_body = Vec::new();
2243 fd_body.extend_from_slice(&file_id_bytes);
2244 fd_body.extend_from_slice(&hash_full);
2245 fd_body.extend_from_slice(&hash_16k);
2246 fd_body.extend_from_slice(&file_length.to_le_bytes());
2247 fd_body.extend_from_slice(filename.as_bytes());
2248 while fd_body.len() % 4 != 0 {
2249 fd_body.push(0);
2250 }
2251 fd_bodies.push(fd_body);
2252
2253 let mut ifsc_body = Vec::new();
2254 ifsc_body.extend_from_slice(&file_id_bytes);
2255 for cs in &checksums {
2256 ifsc_body.extend_from_slice(&cs.md5);
2257 ifsc_body.extend_from_slice(&cs.crc32.to_le_bytes());
2258 }
2259 ifsc_bodies.push(ifsc_body);
2260 }
2261
2262 let mut main_body = Vec::new();
2263 main_body.extend_from_slice(&slice_size.to_le_bytes());
2264 main_body.extend_from_slice(&(file_ids.len() as u32).to_le_bytes());
2265 for file_id in &file_ids {
2266 main_body.extend_from_slice(file_id.as_bytes());
2267 }
2268 let rsid: [u8; 16] = Md5::digest(&main_body).into();
2269
2270 let mut stream = Vec::new();
2271 stream.extend_from_slice(&make_full_packet(header::TYPE_MAIN, &main_body, rsid));
2272 for fd_body in &fd_bodies {
2273 stream.extend_from_slice(&make_full_packet(header::TYPE_FILE_DESC, fd_body, rsid));
2274 }
2275 for ifsc_body in &ifsc_bodies {
2276 stream.extend_from_slice(&make_full_packet(header::TYPE_IFSC, ifsc_body, rsid));
2277 }
2278
2279 let set = Par2FileSet::from_files(&[&stream]).unwrap();
2280 (set, access, file_ids)
2281 }
2282
2283 fn setup_oversized_file_set() -> Par2FileSet {
2284 let slice_size = 4u64;
2285 let file_length = (MAX_SLICES_PER_FILE as u64 + 1) * slice_size;
2286 let filename = b"huge.dat";
2287 let hash_full = [0u8; 16];
2288 let hash_16k = [0u8; 16];
2289
2290 let mut id_input = Vec::new();
2291 id_input.extend_from_slice(&hash_16k);
2292 id_input.extend_from_slice(&file_length.to_le_bytes());
2293 id_input.extend_from_slice(filename);
2294 let file_id_bytes: [u8; 16] = Md5::digest(&id_input).into();
2295
2296 let mut main_body = Vec::new();
2297 main_body.extend_from_slice(&slice_size.to_le_bytes());
2298 main_body.extend_from_slice(&1u32.to_le_bytes());
2299 main_body.extend_from_slice(&file_id_bytes);
2300 let rsid: [u8; 16] = Md5::digest(&main_body).into();
2301
2302 let mut fd_body = Vec::new();
2303 fd_body.extend_from_slice(&file_id_bytes);
2304 fd_body.extend_from_slice(&hash_full);
2305 fd_body.extend_from_slice(&hash_16k);
2306 fd_body.extend_from_slice(&file_length.to_le_bytes());
2307 fd_body.extend_from_slice(filename);
2308 while fd_body.len() % 4 != 0 {
2309 fd_body.push(0);
2310 }
2311
2312 let mut stream = Vec::new();
2313 stream.extend_from_slice(&make_full_packet(header::TYPE_MAIN, &main_body, rsid));
2314 stream.extend_from_slice(&make_full_packet(header::TYPE_FILE_DESC, &fd_body, rsid));
2315
2316 Par2FileSet::from_files(&[&stream]).unwrap()
2317 }
2318
2319 struct ReadIntoOnlyAccess {
2320 files: HashMap<FileId, Vec<u8>>,
2321 }
2322
2323 impl FileAccess for ReadIntoOnlyAccess {
2324 fn read_file_range(
2325 &self,
2326 _file_id: &FileId,
2327 _offset: u64,
2328 _len: u64,
2329 ) -> io::Result<Vec<u8>> {
2330 panic!("read_file_range should not be used by quick_check_16k")
2331 }
2332
2333 fn read_file_range_into(
2334 &self,
2335 file_id: &FileId,
2336 offset: u64,
2337 dst: &mut [u8],
2338 ) -> io::Result<usize> {
2339 let data = self
2340 .files
2341 .get(file_id)
2342 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))?;
2343 let offset = offset as usize;
2344 if offset >= data.len() {
2345 return Ok(0);
2346 }
2347 let end = (offset + dst.len()).min(data.len());
2348 let read_len = end - offset;
2349 dst[..read_len].copy_from_slice(&data[offset..end]);
2350 Ok(read_len)
2351 }
2352
2353 fn file_exists(&self, file_id: &FileId) -> bool {
2354 self.files.contains_key(file_id)
2355 }
2356
2357 fn file_length(&self, file_id: &FileId) -> Option<u64> {
2358 self.files.get(file_id).map(|data| data.len() as u64)
2359 }
2360
2361 fn read_file(&self, file_id: &FileId) -> io::Result<Vec<u8>> {
2362 self.files
2363 .get(file_id)
2364 .cloned()
2365 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))
2366 }
2367
2368 fn write_file_range(
2369 &mut self,
2370 _file_id: &FileId,
2371 _offset: u64,
2372 _data: &[u8],
2373 ) -> io::Result<()> {
2374 Err(io::Error::new(
2375 io::ErrorKind::Unsupported,
2376 "test access is read-only",
2377 ))
2378 }
2379 }
2380
2381 struct SequentialOnlyAccess {
2382 files: HashMap<FileId, Vec<u8>>,
2383 }
2384
2385 impl FileAccess for SequentialOnlyAccess {
2386 fn read_file_range(
2387 &self,
2388 _file_id: &FileId,
2389 _offset: u64,
2390 _len: u64,
2391 ) -> io::Result<Vec<u8>> {
2392 panic!("read_file_range should not be used when a sequential reader is available")
2393 }
2394
2395 fn read_file_range_into(
2396 &self,
2397 _file_id: &FileId,
2398 _offset: u64,
2399 _dst: &mut [u8],
2400 ) -> io::Result<usize> {
2401 panic!("read_file_range_into should not be used when a sequential reader is available")
2402 }
2403
2404 fn open_sequential_reader(&self, file_id: &FileId) -> io::Result<Option<Box<dyn Read>>> {
2405 let data = self
2406 .files
2407 .get(file_id)
2408 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))?;
2409 Ok(Some(Box::new(Cursor::new(data.clone()))))
2410 }
2411
2412 fn file_exists(&self, file_id: &FileId) -> bool {
2413 self.files.contains_key(file_id)
2414 }
2415
2416 fn file_length(&self, file_id: &FileId) -> Option<u64> {
2417 self.files.get(file_id).map(|data| data.len() as u64)
2418 }
2419
2420 fn read_file(&self, file_id: &FileId) -> io::Result<Vec<u8>> {
2421 self.files
2422 .get(file_id)
2423 .cloned()
2424 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))
2425 }
2426
2427 fn write_file_range(
2428 &mut self,
2429 _file_id: &FileId,
2430 _offset: u64,
2431 _data: &[u8],
2432 ) -> io::Result<()> {
2433 Err(io::Error::new(
2434 io::ErrorKind::Unsupported,
2435 "test access is read-only",
2436 ))
2437 }
2438 }
2439
2440 struct CountingSequentialAccess {
2441 files: HashMap<FileId, Vec<u8>>,
2442 open_calls: Arc<AtomicUsize>,
2443 }
2444
2445 impl FileAccess for CountingSequentialAccess {
2446 fn read_file_range(
2447 &self,
2448 _file_id: &FileId,
2449 _offset: u64,
2450 _len: u64,
2451 ) -> io::Result<Vec<u8>> {
2452 panic!("read_file_range should not be used when a sequential reader is available")
2453 }
2454
2455 fn read_file_range_into(
2456 &self,
2457 _file_id: &FileId,
2458 _offset: u64,
2459 _dst: &mut [u8],
2460 ) -> io::Result<usize> {
2461 panic!("read_file_range_into should not be used when a sequential reader is available")
2462 }
2463
2464 fn open_sequential_reader(&self, file_id: &FileId) -> io::Result<Option<Box<dyn Read>>> {
2465 self.open_calls.fetch_add(1, Ordering::Relaxed);
2466 let data = self
2467 .files
2468 .get(file_id)
2469 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found"))?;
2470 Ok(Some(Box::new(Cursor::new(data.clone()))))
2471 }
2472
2473 fn file_exists(&self, file_id: &FileId) -> bool {
2474 self.files.contains_key(file_id)
2475 }
2476
2477 fn file_length(&self, file_id: &FileId) -> Option<u64> {
2478 self.files.get(file_id).map(|data| data.len() as u64)
2479 }
2480
2481 fn read_file(&self, _file_id: &FileId) -> io::Result<Vec<u8>> {
2482 Err(io::Error::new(
2483 io::ErrorKind::Unsupported,
2484 "force streaming path in this test",
2485 ))
2486 }
2487
2488 fn write_file_range(
2489 &mut self,
2490 _file_id: &FileId,
2491 _offset: u64,
2492 _data: &[u8],
2493 ) -> io::Result<()> {
2494 Err(io::Error::new(
2495 io::ErrorKind::Unsupported,
2496 "test access is read-only",
2497 ))
2498 }
2499 }
2500
2501 #[test]
2502 fn verify_slices_from_crcs_intact() {
2503 let file_data = vec![0xABu8; 2048];
2504 let (set, _access, file_id) = setup_test_set(&file_data, 1024);
2505
2506 let crc0 = checksum::crc32(&file_data[0..1024]);
2508 let crc1 = checksum::crc32(&file_data[1024..2048]);
2509
2510 let result = verify_slices_from_crcs(&set, &file_id, &[crc0, crc1]).unwrap();
2511 assert_eq!(result, vec![true, true]);
2512 }
2513
2514 #[test]
2515 fn verify_slices_from_crcs_damaged() {
2516 let file_data = vec![0xABu8; 2048];
2517 let (set, _access, file_id) = setup_test_set(&file_data, 1024);
2518
2519 let crc0 = checksum::crc32(&file_data[0..1024]);
2520 let wrong_crc = 0xDEADBEEF;
2521
2522 let result = verify_slices_from_crcs(&set, &file_id, &[crc0, wrong_crc]).unwrap();
2523 assert_eq!(result, vec![true, false]);
2524 }
2525
2526 #[test]
2527 fn verify_slices_from_crcs_with_padding() {
2528 let file_data = vec![0xCDu8; 1500];
2530 let (set, _access, file_id) = setup_test_set(&file_data, 1024);
2531
2532 let crc0 = checksum::crc32(&file_data[0..1024]);
2533 let mut padded_last = file_data[1024..1500].to_vec();
2535 padded_last.resize(1024, 0);
2536 let crc1 = checksum::crc32(&padded_last);
2537
2538 let result = verify_slices_from_crcs(&set, &file_id, &[crc0, crc1]).unwrap();
2539 assert_eq!(result, vec![true, true]);
2540 }
2541
2542 #[test]
2543 fn verify_intact_file() {
2544 let file_data = vec![0xABu8; 2048];
2545 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2546
2547 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(true));
2549
2550 assert_eq!(verify_full_hash(&set, &file_id, &access), Some(true));
2552
2553 let slices = verify_slices(&set, &file_id, &access).unwrap();
2555 assert_eq!(slices.len(), 2);
2556 assert!(slices.iter().all(|&v| v));
2557
2558 let result = verify_all(&set, &access);
2560 assert_eq!(result.files.len(), 1);
2561 assert!(matches!(result.files[0].status, FileStatus::Complete));
2562 assert_eq!(result.total_missing_blocks, 0);
2563 assert!(matches!(result.repairable, Repairability::NotNeeded));
2564 }
2565
2566 #[test]
2567 fn verify_damaged_file() {
2568 let mut file_data = vec![0xABu8; 2048];
2569 let (set, mut access, file_id) = setup_test_set(&file_data, 1024);
2570
2571 file_data[1024] ^= 0xFF;
2573 file_data[1025] ^= 0xFF;
2574 access.add_file(file_id, file_data);
2575
2576 let slices = verify_slices(&set, &file_id, &access).unwrap();
2578 assert!(slices[0]); assert!(!slices[1]); let result = verify_all(&set, &access);
2583 assert_eq!(result.files.len(), 1);
2584 assert!(matches!(result.files[0].status, FileStatus::Damaged(1)));
2585 assert_eq!(result.total_missing_blocks, 1);
2586 }
2587
2588 #[test]
2589 fn verify_missing_file() {
2590 let file_data = vec![0xABu8; 2048];
2591 let (set, _, _file_id) = setup_test_set(&file_data, 1024);
2592
2593 let access = MemoryFileAccess::new();
2595
2596 let result = verify_all(&set, &access);
2597 assert_eq!(result.files.len(), 1);
2598 assert!(matches!(result.files[0].status, FileStatus::Missing));
2599 assert_eq!(result.total_missing_blocks, 2);
2600 }
2601
2602 #[test]
2603 fn verify_file_with_partial_last_slice() {
2604 let file_data = vec![0xCDu8; 1500];
2606 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2607
2608 let slices = verify_slices(&set, &file_id, &access).unwrap();
2609 assert_eq!(slices.len(), 2);
2610 assert!(slices[0]);
2611 assert!(slices[1]); }
2613
2614 #[test]
2615 fn quick_check_missing_file() {
2616 let file_data = vec![0xABu8; 100];
2617 let (set, _, file_id) = setup_test_set(&file_data, 1024);
2618 let access = MemoryFileAccess::new();
2619
2620 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(false));
2621 }
2622
2623 #[test]
2624 fn quick_check_short_file_matches_and_detects_corruption() {
2625 let file_data = b"short-par2-file".to_vec();
2626 let (set, mut access, file_id) = setup_test_set(&file_data, 1024);
2627
2628 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(true));
2629
2630 let mut corrupted = file_data.clone();
2631 corrupted[3] ^= 0xFF;
2632 access.add_file(file_id, corrupted);
2633
2634 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(false));
2635 }
2636
2637 #[test]
2638 fn quick_check_exact_16k_boundary_matches_and_detects_corruption() {
2639 let file_data = (0..QUICK_CHECK_16K_BYTES)
2640 .map(|i| (i % 251) as u8)
2641 .collect::<Vec<_>>();
2642 let (set, mut access, file_id) = setup_test_set(&file_data, 4096);
2643
2644 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(true));
2645
2646 let mut corrupted = file_data.clone();
2647 corrupted[QUICK_CHECK_16K_BYTES - 1] ^= 0x55;
2648 access.add_file(file_id, corrupted);
2649
2650 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(false));
2651 }
2652
2653 #[test]
2654 fn quick_check_uses_read_into_path() {
2655 let file_data = vec![0x5Au8; 4096];
2656 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2657 let access = ReadIntoOnlyAccess {
2658 files: access.files,
2659 };
2660
2661 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(true));
2662 }
2663
2664 #[test]
2665 fn quick_check_uses_sequential_reader_when_available() {
2666 let file_data = vec![0x6Bu8; 4096];
2667 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2668 let access = SequentialOnlyAccess {
2669 files: access.files,
2670 };
2671
2672 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(true));
2673 }
2674
2675 #[test]
2676 fn verify_full_hash_uses_sequential_reader_when_available() {
2677 let file_data = (0..(QUICK_CHECK_16K_BYTES + 4096))
2678 .map(|i| (i % 251) as u8)
2679 .collect::<Vec<_>>();
2680 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2681 let access = SequentialOnlyAccess {
2682 files: access.files,
2683 };
2684
2685 assert_eq!(verify_full_hash(&set, &file_id, &access), Some(true));
2686 }
2687
2688 #[test]
2689 fn verify_selected_file_ids_uses_single_sequential_pass_for_healthy_file() {
2690 let file_data = (0..(QUICK_CHECK_16K_BYTES + 4096))
2691 .map(|i| (i % 241) as u8)
2692 .collect::<Vec<_>>();
2693 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2694 let open_calls = Arc::new(AtomicUsize::new(0));
2695 let access = CountingSequentialAccess {
2696 files: access.files,
2697 open_calls: open_calls.clone(),
2698 };
2699
2700 let verification = verify_selected_file_ids_resolved(
2705 &set,
2706 &access,
2707 None,
2708 &[file_id],
2709 &VerifyOptions::default(),
2710 false,
2711 );
2712
2713 assert_eq!(verification.total_missing_blocks, 0);
2714 assert!(matches!(verification.files[0].status, FileStatus::Complete));
2715 assert_eq!(open_calls.load(Ordering::Relaxed), 1);
2716 }
2717
2718 #[test]
2719 fn verify_slices_uses_batched_sequential_reader_when_available() {
2720 let file_data = (0..8192)
2721 .map(|i| (i as u8).wrapping_mul(13).wrapping_add(7))
2722 .collect::<Vec<_>>();
2723 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2724 let access = SequentialOnlyAccess {
2725 files: access.files,
2726 };
2727
2728 let slices = verify_slices(&set, &file_id, &access).unwrap();
2729 assert_eq!(slices, vec![true; 8]);
2730 }
2731
2732 #[test]
2733 fn verifier_falls_back_to_read_into_when_no_sequential_reader_exists() {
2734 let file_data = (0..8192)
2735 .map(|i| (i as u8).wrapping_mul(17).wrapping_add(11))
2736 .collect::<Vec<_>>();
2737 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2738 let access = ReadIntoOnlyAccess {
2739 files: access.files,
2740 };
2741
2742 assert_eq!(verify_full_hash(&set, &file_id, &access), Some(true));
2743 assert_eq!(verify_slices(&set, &file_id, &access), Some(vec![true; 8]));
2744 }
2745
2746 #[test]
2747 fn verify_selected_file_ids_reuses_quick_check_scratch_without_allocating_reads() {
2748 let file_a = vec![0x11u8; 2048];
2749 let file_b = (0..(QUICK_CHECK_16K_BYTES + 257))
2750 .map(|i| (i % 239) as u8)
2751 .collect::<Vec<_>>();
2752 let files = [
2753 (file_a.as_slice(), "alpha.bin"),
2754 (file_b.as_slice(), "beta.bin"),
2755 ];
2756 let (set, access, file_ids) = setup_test_set_multi(&files, 1024);
2757 let access = ReadIntoOnlyAccess {
2758 files: access.files,
2759 };
2760
2761 let verification = verify_selected_file_ids_resolved(
2765 &set,
2766 &access,
2767 None,
2768 &file_ids,
2769 &VerifyOptions::default(),
2770 false,
2771 );
2772
2773 assert_eq!(verification.files.len(), 2);
2774 assert!(
2775 verification
2776 .files
2777 .iter()
2778 .all(|file| matches!(file.status, FileStatus::Complete))
2779 );
2780 assert_eq!(verification.total_missing_blocks, 0);
2781 }
2782
2783 #[test]
2784 fn verify_repairable_assessment() {
2785 let file_data = vec![0xABu8; 4096];
2786 let (mut set, mut access, file_id) = setup_test_set(&file_data, 1024);
2787
2788 let mut corrupted = file_data.clone();
2790 corrupted[0] ^= 0xFF;
2791 access.add_file(file_id, corrupted);
2792
2793 use crate::par2_set::RecoverySlice;
2795 use bytes::Bytes;
2796 set.recovery_slices.insert(
2797 0,
2798 RecoverySlice {
2799 exponent: 0,
2800 data: Bytes::from(vec![0u8; 1024]).into(),
2801 },
2802 );
2803 set.recovery_slices.insert(
2804 1,
2805 RecoverySlice {
2806 exponent: 1,
2807 data: Bytes::from(vec![0u8; 1024]).into(),
2808 },
2809 );
2810
2811 let result = verify_all(&set, &access);
2812 assert!(matches!(
2814 result.repairable,
2815 Repairability::Repairable {
2816 blocks_needed: 1,
2817 blocks_available: 2
2818 }
2819 ));
2820 }
2821
2822 #[test]
2823 fn verify_insufficient_recovery() {
2824 let file_data = vec![0xABu8; 4096];
2825 let (set, _, _file_id) = setup_test_set(&file_data, 1024);
2826 let access = MemoryFileAccess::new(); let result = verify_all(&set, &access);
2829 assert!(matches!(
2830 result.repairable,
2831 Repairability::Insufficient { .. }
2832 ));
2833 }
2834
2835 #[test]
2836 fn verify_resource_limited_file_does_not_inflate_missing_blocks() {
2837 let set = setup_oversized_file_set();
2838 let access = MemoryFileAccess::new();
2839
2840 let result = verify_all(&set, &access);
2841
2842 assert_eq!(result.total_missing_blocks, 0);
2843 assert_eq!(result.files.len(), 1);
2844 assert_eq!(result.files[0].missing_slice_count, 0);
2845 assert!(result.files[0].valid_slices.is_empty());
2846 assert!(matches!(result.files[0].status, FileStatus::Damaged(0)));
2847 match result.repairable {
2848 Repairability::ResourceLimited { reason } => {
2849 assert!(reason.contains("huge.dat"));
2850 }
2851 other => panic!("expected resource-limited repairability, got {other:?}"),
2852 }
2853 }
2854
2855 #[test]
2856 fn verify_large_file_multiple_slices() {
2857 let file_data: Vec<u8> = (0..10240u32).map(|i| (i % 256) as u8).collect();
2859 let (set, access, file_id) = setup_test_set(&file_data, 1024);
2860
2861 let slices = verify_slices(&set, &file_id, &access).unwrap();
2862 assert_eq!(slices.len(), 10);
2863 assert!(slices.iter().all(|&v| v));
2864 }
2865
2866 #[test]
2867 fn verify_corrupted_first_slice_16k_check_fails() {
2868 let file_data = vec![0xABu8; 4096];
2869 let (set, mut access, file_id) = setup_test_set(&file_data, 1024);
2870
2871 let mut corrupted = file_data.clone();
2873 corrupted[0] ^= 0xFF;
2874 access.add_file(file_id, corrupted);
2875
2876 assert_eq!(quick_check_16k(&set, &file_id, &access), Some(false));
2877 }
2878
2879 #[test]
2880 fn verify_selected_file_ids_only_counts_requested_files() {
2881 let good = vec![0x11u8; 2048];
2882 let damaged = vec![0x22u8; 2048];
2883 let (set, mut access, file_ids) =
2884 setup_test_set_multi(&[(&good, "good.rar"), (&damaged, "damaged.rar")], 1024);
2885
2886 let mut corrupted = damaged.clone();
2887 corrupted[0] ^= 0xFF;
2888 access.add_file(file_ids[1], corrupted);
2889
2890 let selected = verify_selected_file_ids(&set, &access, &[file_ids[0]]);
2891 assert_eq!(selected.files.len(), 1);
2892 assert_eq!(selected.total_missing_blocks, 0);
2893 assert!(matches!(selected.repairable, Repairability::NotNeeded));
2894
2895 let damaged_only = verify_selected_file_ids(&set, &access, &[file_ids[1]]);
2896 assert_eq!(damaged_only.files.len(), 1);
2897 assert_eq!(damaged_only.total_missing_blocks, 1);
2898 assert!(matches!(
2899 damaged_only.files[0].status,
2900 FileStatus::Damaged(1)
2901 ));
2902 }
2903
2904 #[test]
2905 fn parallel_selected_verify_matches_serial() {
2906 let intact = vec![0x11u8; 4096];
2907 let damaged = vec![0x22u8; 4096];
2908 let truncated = vec![0x33u8; 4096];
2909 let (set, mut access, file_ids) = setup_test_set_multi(
2910 &[
2911 (&intact, "intact.rar"),
2912 (&damaged, "damaged.rar"),
2913 (&truncated, "truncated.rar"),
2914 ],
2915 1024,
2916 );
2917
2918 let mut corrupted = damaged.clone();
2919 corrupted[1500] ^= 0xFF;
2920 corrupted[3000] ^= 0xFF;
2921 access.add_file(file_ids[1], corrupted);
2922 access.add_file(file_ids[2], truncated[..2500].to_vec());
2923
2924 let serial = verify_selected_file_ids(&set, &access, &file_ids);
2925 let parallel = verify_selected_file_ids_parallel(&set, &access, &file_ids);
2926 assert!(serial.total_missing_blocks > 0, "fixture must have damage");
2927 assert_eq!(
2928 format!("{serial:?}"),
2929 format!("{parallel:?}"),
2930 "parallel selected verify must match the serial pipeline exactly"
2931 );
2932
2933 let base = verify_selected_file_ids(&set, &access, &file_ids);
2936 let fixed_subset = {
2937 let mut fixed = MemoryFileAccess::new();
2938 fixed.add_file(file_ids[0], intact.clone());
2939 fixed.add_file(file_ids[1], damaged.clone());
2940 fixed.add_file(file_ids[2], truncated.clone());
2941 verify_selected_file_ids_parallel(&set, &fixed, &file_ids[1..])
2942 };
2943 let merged = merge_verification_results(&set, &base, fixed_subset);
2944 assert_eq!(merged.files.len(), file_ids.len());
2945 assert_eq!(merged.total_missing_blocks, 0);
2946 assert!(
2947 merged
2948 .files
2949 .iter()
2950 .all(|file| matches!(file.status, FileStatus::Complete)),
2951 "merged result must show all files complete: {merged:?}"
2952 );
2953 assert!(matches!(merged.repairable, Repairability::NotNeeded));
2954 }
2955
2956 fn setup_test_set_no_ifsc(
2960 file_data: &[u8],
2961 slice_size: u64,
2962 ) -> (Par2FileSet, MemoryFileAccess, FileId) {
2963 let file_length = file_data.len() as u64;
2964 let hash_full = checksum::md5(file_data);
2965 let hash_16k = checksum::md5(&file_data[..file_data.len().min(16384)]);
2966
2967 let filename = b"testfile.dat";
2968 let mut id_input = Vec::new();
2969 id_input.extend_from_slice(&hash_16k);
2970 id_input.extend_from_slice(&file_length.to_le_bytes());
2971 id_input.extend_from_slice(filename);
2972 let file_id_bytes: [u8; 16] = Md5::digest(&id_input).into();
2973 let file_id = FileId::from_bytes(file_id_bytes);
2974
2975 let mut main_body = Vec::new();
2976 main_body.extend_from_slice(&slice_size.to_le_bytes());
2977 main_body.extend_from_slice(&1u32.to_le_bytes());
2978 main_body.extend_from_slice(&file_id_bytes);
2979 let rsid: [u8; 16] = Md5::digest(&main_body).into();
2980
2981 let mut fd_body = Vec::new();
2982 fd_body.extend_from_slice(&file_id_bytes);
2983 fd_body.extend_from_slice(&hash_full);
2984 fd_body.extend_from_slice(&hash_16k);
2985 fd_body.extend_from_slice(&file_length.to_le_bytes());
2986 fd_body.extend_from_slice(filename);
2987 while fd_body.len() % 4 != 0 {
2988 fd_body.push(0);
2989 }
2990
2991 let mut stream = Vec::new();
2992 stream.extend_from_slice(&make_full_packet(header::TYPE_MAIN, &main_body, rsid));
2993 stream.extend_from_slice(&make_full_packet(header::TYPE_FILE_DESC, &fd_body, rsid));
2994
2995 let set = Par2FileSet::from_files(&[&stream]).unwrap();
2996 let mut access = MemoryFileAccess::new();
2997 access.add_file(file_id, file_data.to_vec());
2998 (set, access, file_id)
2999 }
3000
3001 fn strict_opts() -> VerifyOptions {
3002 VerifyOptions::default()
3003 }
3004
3005 fn fast_opts() -> VerifyOptions {
3006 VerifyOptions {
3007 fast_verify: true,
3008 ..Default::default()
3009 }
3010 }
3011
3012 #[test]
3013 fn resolve_fast_verify_precedence() {
3014 assert!(resolve_fast_verify(Some("1"), false));
3016 assert!(!resolve_fast_verify(Some("0"), true));
3017 assert!(resolve_fast_verify(None, true));
3019 assert!(!resolve_fast_verify(None, false));
3020 assert!(resolve_fast_verify(Some("yes"), true));
3021 assert!(!resolve_fast_verify(Some(""), false));
3022 }
3023
3024 #[test]
3025 fn fast_verify_intact_matches_strict() {
3026 let file_data = vec![0xA7u8; 1500];
3028 let (set, access, file_id) = setup_test_set(&file_data, 1024);
3029
3030 let strict =
3031 verify_selected_file_ids_with_options(&set, &access, &[file_id], &strict_opts());
3032 let fast = verify_selected_file_ids_with_options(&set, &access, &[file_id], &fast_opts());
3033
3034 assert!(matches!(fast.files[0].status, FileStatus::Complete));
3035 assert_eq!(fast.files[0].valid_slices, vec![true; 2]);
3036 assert_eq!(fast.total_missing_blocks, 0);
3037 assert!(matches!(fast.repairable, Repairability::NotNeeded));
3038 assert_eq!(
3039 format!("{strict:?}"),
3040 format!("{fast:?}"),
3041 "fast intact result must match strict byte for byte"
3042 );
3043 }
3044
3045 #[test]
3046 fn fast_verify_uses_only_non_allocating_reads() {
3047 let file_data = (0..(QUICK_CHECK_16K_BYTES + 1500))
3054 .map(|i| (i % 251) as u8)
3055 .collect::<Vec<_>>();
3056 let (set, access, file_id) = setup_test_set(&file_data, 1024);
3057 let access = ReadIntoOnlyAccess {
3058 files: access.files,
3059 };
3060
3061 let result = verify_selected_file_ids_resolved(
3062 &set,
3063 &access,
3064 None,
3065 &[file_id],
3066 &VerifyOptions::default(),
3067 true,
3068 );
3069
3070 assert!(matches!(result.files[0].status, FileStatus::Complete));
3071 assert_eq!(result.total_missing_blocks, 0);
3072 assert_eq!(result.files[0].valid_slices, vec![true; 18]);
3073 }
3074
3075 #[test]
3076 fn fast_verify_damaged_matches_strict() {
3077 let base: Vec<u8> = (0..20480u32).map(|i| (i % 256) as u8).collect();
3083
3084 for corrupt_offset in [100usize, 18_000usize] {
3085 let (set, mut access, file_id) = setup_test_set(&base, 1024);
3086 let mut corrupted = base.clone();
3087 corrupted[corrupt_offset] ^= 0xFF;
3088 access.add_file(file_id, corrupted);
3089
3090 let strict =
3091 verify_selected_file_ids_with_options(&set, &access, &[file_id], &strict_opts());
3092 let fast =
3093 verify_selected_file_ids_with_options(&set, &access, &[file_id], &fast_opts());
3094
3095 assert!(
3096 matches!(fast.files[0].status, FileStatus::Damaged(1)),
3097 "offset {corrupt_offset}: unexpected status {:?}",
3098 fast.files[0].status
3099 );
3100 assert_eq!(fast.total_missing_blocks, 1, "offset {corrupt_offset}");
3101 let damaged_slice = corrupt_offset / 1024;
3102 assert!(
3103 !fast.files[0].valid_slices[damaged_slice],
3104 "offset {corrupt_offset}: slice {damaged_slice} should be damaged"
3105 );
3106 assert_eq!(
3107 fast.files[0].valid_slices.iter().filter(|v| !**v).count(),
3108 1,
3109 "offset {corrupt_offset}: exactly one slice damaged"
3110 );
3111 assert_eq!(
3112 format!("{strict:?}"),
3113 format!("{fast:?}"),
3114 "fast and strict must agree for corruption at offset {corrupt_offset}"
3115 );
3116 }
3117 }
3118
3119 #[test]
3120 fn fast_verify_falls_back_without_ifsc() {
3121 let file_data = vec![0x33u8; 4096];
3122 let (set, access, file_id) = setup_test_set_no_ifsc(&file_data, 1024);
3123
3124 let strict =
3125 verify_selected_file_ids_with_options(&set, &access, &[file_id], &strict_opts());
3126 let fast = verify_selected_file_ids_with_options(&set, &access, &[file_id], &fast_opts());
3127
3128 assert!(matches!(fast.files[0].status, FileStatus::Complete));
3129 assert_eq!(fast.total_missing_blocks, 0);
3130 assert_eq!(
3131 format!("{strict:?}"),
3132 format!("{fast:?}"),
3133 "without IFSC, fast verify must fall back to the serial pipeline"
3134 );
3135 }
3136
3137 #[test]
3138 fn fast_verify_length_mismatch_falls_back() {
3139 let file_data = vec![0x5Cu8; 4096];
3140 let (set, mut access, file_id) = setup_test_set(&file_data, 1024);
3141 access.add_file(file_id, file_data[..2500].to_vec());
3145
3146 let strict =
3147 verify_selected_file_ids_with_options(&set, &access, &[file_id], &strict_opts());
3148 let fast = verify_selected_file_ids_with_options(&set, &access, &[file_id], &fast_opts());
3149
3150 assert!(matches!(fast.files[0].status, FileStatus::Damaged(_)));
3151 assert_eq!(
3152 format!("{strict:?}"),
3153 format!("{fast:?}"),
3154 "length mismatch must fall back identically to strict"
3155 );
3156 }
3157
3158 #[test]
3159 fn fast_verify_flag_off_matches_strict_expectation() {
3160 let intact = vec![0x11u8; 4096];
3163 let damaged = vec![0x22u8; 4096];
3164 let (set, mut access, file_ids) =
3165 setup_test_set_multi(&[(&intact, "intact.rar"), (&damaged, "damaged.rar")], 1024);
3166 let mut corrupted = damaged.clone();
3167 corrupted[0] ^= 0xFF;
3168 access.add_file(file_ids[1], corrupted);
3169
3170 let off = verify_selected_file_ids_with_options(
3171 &set,
3172 &access,
3173 &file_ids,
3174 &VerifyOptions::default(),
3175 );
3176 assert!(matches!(off.files[0].status, FileStatus::Complete));
3177 assert!(matches!(off.files[1].status, FileStatus::Damaged(1)));
3178 assert_eq!(off.total_missing_blocks, 1);
3179
3180 let strict = verify_selected_file_ids(&set, &access, &file_ids);
3182 assert_eq!(format!("{off:?}"), format!("{strict:?}"));
3183 }
3184
3185 #[test]
3186 fn fast_verify_parallel_matches_strict() {
3187 let intact = vec![0x11u8; 4096];
3188 let damaged = vec![0x22u8; 4096];
3189 let truncated = vec![0x33u8; 4096];
3190 let (set, mut access, file_ids) = setup_test_set_multi(
3191 &[
3192 (&intact, "intact.rar"),
3193 (&damaged, "damaged.rar"),
3194 (&truncated, "truncated.rar"),
3195 ],
3196 1024,
3197 );
3198 let mut corrupted = damaged.clone();
3199 corrupted[1500] ^= 0xFF;
3200 access.add_file(file_ids[1], corrupted);
3201 access.add_file(file_ids[2], truncated[..2500].to_vec());
3202
3203 let strict = verify_selected_file_ids_parallel(&set, &access, &file_ids);
3205 let fast =
3206 verify_selected_file_ids_parallel_with_options(&set, &access, &file_ids, &fast_opts());
3207 assert!(fast.total_missing_blocks > 0, "fixture must have damage");
3208 assert_eq!(
3209 format!("{strict:?}"),
3210 format!("{fast:?}"),
3211 "parallel fast verify must match parallel strict verify"
3212 );
3213
3214 for id in &file_ids {
3216 let strict_one =
3217 verify_selected_file_ids_parallel(&set, &access, std::slice::from_ref(id));
3218 let fast_one = verify_selected_file_ids_parallel_with_options(
3219 &set,
3220 &access,
3221 std::slice::from_ref(id),
3222 &fast_opts(),
3223 );
3224 assert_eq!(
3225 format!("{strict_one:?}"),
3226 format!("{fast_one:?}"),
3227 "single-file span-parallel fast verify must match strict"
3228 );
3229 }
3230 }
3231
3232 #[test]
3233 fn refresh_repairability_recomputes_after_missing_count_changes() {
3234 let mut result = VerificationResult {
3235 files: Vec::new(),
3236 recovery_blocks_available: 40,
3237 total_missing_blocks: 1380,
3238 repairable: Repairability::Insufficient {
3239 blocks_needed: 1380,
3240 blocks_available: 40,
3241 deficit: 1340,
3242 },
3243 };
3244
3245 result.total_missing_blocks = 40;
3246 result.refresh_repairability();
3247
3248 assert!(matches!(
3249 result.repairable,
3250 Repairability::Repairable {
3251 blocks_needed: 40,
3252 blocks_available: 40
3253 }
3254 ));
3255 }
3256
3257 fn deterministic_file(len: usize) -> Vec<u8> {
3258 (0..len).map(|i| (i % 251) as u8).collect()
3259 }
3260
3261 #[test]
3267 fn strict_verify_reports_verify_slices_vector_for_every_damage_position() {
3268 for slice_size in [1024u64, (VERIFY_FULL_HASH_CHUNK_BYTES as u64) + 512] {
3269 let len = (slice_size as usize) * 5 + 300;
3270 let pristine = deterministic_file(len);
3271 let slice_count = (len as u64).div_ceil(slice_size) as usize;
3272 for damaged_slice in 0..slice_count {
3273 let mut data = pristine.clone();
3274 let at = (damaged_slice as u64 * slice_size) as usize + 7;
3275 data[at] ^= 0xff;
3276 let (set, _, file_id) = setup_test_set(&pristine, slice_size);
3277 let mut access = MemoryFileAccess::new();
3278 access.add_file(file_id, data);
3279
3280 let expected = verify_slices(&set, &file_id, &access).expect("slice vector");
3281 assert!(!expected[damaged_slice], "slice {damaged_slice} must fail");
3282 let expected_damaged = expected.iter().filter(|valid| !**valid).count() as u32;
3283
3284 for span_access in [None, Some(&access as &(dyn FileAccess + Sync))] {
3285 let result = verify_selected_file_ids_resolved(
3286 &set,
3287 &access,
3288 span_access,
3289 &[file_id],
3290 &VerifyOptions::default(),
3291 false,
3292 );
3293 let file = &result.files[0];
3294 assert!(
3295 matches!(file.status, FileStatus::Damaged(count) if count == expected_damaged),
3296 "slice_size {slice_size} damaged slice {damaged_slice}: {:?}",
3297 file.status
3298 );
3299 assert_eq!(file.valid_slices, expected);
3300 assert_eq!(file.missing_slice_count, expected_damaged);
3301 assert_eq!(result.total_missing_blocks, expected_damaged);
3302 }
3303 }
3304 }
3305 }
3306
3307 #[test]
3310 fn strict_verify_still_requires_the_whole_file_hash_to_report_complete() {
3311 let slice_size = 1024u64;
3312 let data = deterministic_file((slice_size as usize) * 4 + 11);
3313 let (set, access, file_id) = setup_test_set(&data, slice_size);
3314
3315 for span_access in [None, Some(&access as &(dyn FileAccess + Sync))] {
3316 let result = verify_selected_file_ids_resolved(
3317 &set,
3318 &access,
3319 span_access,
3320 &[file_id],
3321 &VerifyOptions::default(),
3322 false,
3323 );
3324 assert!(matches!(result.files[0].status, FileStatus::Complete));
3325 assert_eq!(result.files[0].valid_slices, vec![true; 5]);
3326 assert_eq!(result.total_missing_blocks, 0);
3327 }
3328 }
3329
3330 #[test]
3335 fn strict_verify_reports_damaged_zero_when_only_the_whole_file_hash_disagrees() {
3336 let slice_size = 4096u64;
3339 let data = deterministic_file((slice_size as usize) * 6);
3340 let (set, access, file_id) =
3341 setup_test_set_with_full_hash(&data, slice_size, Some([0x5a; 16]));
3342
3343 for span_access in [None, Some(&access as &(dyn FileAccess + Sync))] {
3344 let result = verify_selected_file_ids_resolved(
3345 &set,
3346 &access,
3347 span_access,
3348 &[file_id],
3349 &VerifyOptions::default(),
3350 false,
3351 );
3352 assert!(matches!(result.files[0].status, FileStatus::Damaged(0)));
3353 assert_eq!(result.files[0].valid_slices, vec![true; 6]);
3354 assert_eq!(result.total_missing_blocks, 0);
3355 }
3356 }
3357
3358 #[test]
3361 fn span_parallel_slice_scan_matches_verify_slices() {
3362 let slice_size = 1024u64;
3363 let pristine = deterministic_file((slice_size as usize) * 9 + 617);
3364 let mut data = pristine.clone();
3365 for damaged_slice in [0usize, 3, 9] {
3366 let at = (damaged_slice as u64 * slice_size) as usize + 1;
3367 data[at] ^= 0xff;
3368 }
3369 let (set, _, file_id) = setup_test_set(&pristine, slice_size);
3370 let mut access = MemoryFileAccess::new();
3371 access.add_file(file_id, data);
3372
3373 let serial = verify_slices(&set, &file_id, &access).expect("slice vector");
3374 let spanned = verify_file_sliced(&set, &access, &file_id, true).expect("span scan");
3375 assert_eq!(spanned.valid_slices, serial);
3376 assert_eq!(
3377 strict_slice_validity(&set, &access, Some(&access), &file_id, serial.len()),
3378 serial
3379 );
3380 }
3381
3382 #[test]
3391 #[ignore = "perf measurement, run by hand in release mode"]
3392 fn perf_intact_ride_along_overhead() {
3393 let slice_size = 512 * 1024u64;
3394 let len = 512 * 1024 * 1024usize;
3395 let data = deterministic_file(len);
3396 let (set, access, file_id) = setup_test_set_with_full_hash(&data, slice_size, None);
3397 let checksums = set.file_checksums(&file_id).expect("checksums");
3398
3399 let mut timings: [Vec<f64>; 2] = [Vec::new(), Vec::new()];
3400 for _round in 0..5 {
3402 for (arm, slice_checksums) in [(0usize, None), (1usize, Some(checksums))] {
3403 let started = std::time::Instant::now();
3404 let outcome = stream_strict_hashes(&set, &file_id, &access, slice_checksums)
3405 .expect("stream outcome");
3406 let elapsed = started.elapsed().as_secs_f64();
3407 assert!(matches!(
3408 outcome,
3409 StrictStreamOutcome::Hashes {
3410 quick_ok: true,
3411 full_ok: true
3412 }
3413 ));
3414 timings[arm].push(elapsed);
3415 }
3416 }
3417 let best = |samples: &[f64]| samples.iter().copied().fold(f64::INFINITY, f64::min);
3418 let plain = best(&timings[0]);
3419 let fused = best(&timings[1]);
3420 let mb = len as f64 / (1024.0 * 1024.0);
3421 println!(
3422 "intact strict stream, {len} bytes, slice {slice_size}: \
3423 plain {:.0} MB/s, ride-along {:.0} MB/s, overhead {:+.2}%",
3424 mb / plain,
3425 mb / fused,
3426 (fused / plain - 1.0) * 100.0
3427 );
3428 }
3429
3430 #[test]
3433 fn fused_chunk_size_is_slice_aligned_and_bounded() {
3434 assert_eq!(fused_chunk_bytes(0), None);
3435 assert_eq!(
3436 fused_chunk_bytes((VERIFY_FULL_HASH_CHUNK_BYTES as u64) + 1),
3437 None
3438 );
3439 for slice_size in [2u64, 1024, 65536, VERIFY_FULL_HASH_CHUNK_BYTES as u64] {
3440 let chunk = fused_chunk_bytes(slice_size).expect("chunk");
3441 assert!(chunk > 0);
3442 assert!(chunk <= VERIFY_FULL_HASH_CHUNK_BYTES);
3443 assert_eq!(chunk as u64 % slice_size, 0);
3444 }
3445 }
3446
3447 struct CountingAccess {
3455 inner: MemoryFileAccess,
3456 bytes_read: AtomicUsize,
3457 }
3458
3459 impl CountingAccess {
3460 fn new(inner: MemoryFileAccess) -> Self {
3461 Self {
3462 inner,
3463 bytes_read: AtomicUsize::new(0),
3464 }
3465 }
3466
3467 fn take_bytes_read(&self) -> usize {
3468 self.bytes_read.swap(0, Ordering::Relaxed)
3469 }
3470 }
3471
3472 impl FileAccess for CountingAccess {
3473 fn read_file_range(&self, file_id: &FileId, offset: u64, len: u64) -> io::Result<Vec<u8>> {
3474 let data = self.inner.read_file_range(file_id, offset, len)?;
3475 self.bytes_read.fetch_add(data.len(), Ordering::Relaxed);
3476 Ok(data)
3477 }
3478
3479 fn read_file_range_into(
3480 &self,
3481 file_id: &FileId,
3482 offset: u64,
3483 dst: &mut [u8],
3484 ) -> io::Result<usize> {
3485 let read = self.inner.read_file_range_into(file_id, offset, dst)?;
3486 self.bytes_read.fetch_add(read, Ordering::Relaxed);
3487 Ok(read)
3488 }
3489
3490 fn file_exists(&self, file_id: &FileId) -> bool {
3491 self.inner.file_exists(file_id)
3492 }
3493
3494 fn file_length(&self, file_id: &FileId) -> Option<u64> {
3495 self.inner.file_length(file_id)
3496 }
3497
3498 fn read_file(&self, file_id: &FileId) -> io::Result<Vec<u8>> {
3499 let data = self.inner.read_file(file_id)?;
3500 self.bytes_read.fetch_add(data.len(), Ordering::Relaxed);
3501 Ok(data)
3502 }
3503
3504 fn write_file_range(
3505 &mut self,
3506 file_id: &FileId,
3507 offset: u64,
3508 data: &[u8],
3509 ) -> io::Result<()> {
3510 self.inner.write_file_range(file_id, offset, data)
3511 }
3512 }
3513
3514 fn evidence_opts(proven: HashMap<FileId, Vec<bool>>) -> VerifyOptions {
3515 VerifyOptions {
3516 proven_slices: proven,
3517 ..Default::default()
3518 }
3519 }
3520
3521 fn deterministic_bytes(len: usize, seed: u8) -> Vec<u8> {
3522 (0..len)
3523 .map(|index| ((index as u8).wrapping_mul(31)).wrapping_add(seed))
3524 .collect()
3525 }
3526
3527 fn evidence_corpus(slice_size: u64) -> (Par2FileSet, MemoryFileAccess, Vec<FileId>) {
3532 let intact = deterministic_bytes(4 * slice_size as usize, 1);
3533 let damaged_source = deterministic_bytes(4 * slice_size as usize, 2);
3534 let short_tail = deterministic_bytes(3 * slice_size as usize - 7, 3);
3535 let absent = deterministic_bytes(2 * slice_size as usize, 4);
3536
3537 let (set, _, ids) = setup_test_set_multi(
3538 &[
3539 (&intact, "intact.bin"),
3540 (&damaged_source, "damaged.bin"),
3541 (&short_tail, "short-tail.bin"),
3542 (&absent, "absent.bin"),
3543 ],
3544 slice_size,
3545 );
3546
3547 let mut damaged = damaged_source.clone();
3550 let start = 2 * slice_size as usize;
3551 damaged[start..start + slice_size as usize].fill(0);
3552 let mut short_tail_damaged = short_tail.clone();
3553 short_tail_damaged[..slice_size as usize].fill(0xFF);
3554
3555 let mut access = MemoryFileAccess::new();
3556 access.add_file(ids[0], intact);
3557 access.add_file(ids[1], damaged);
3558 access.add_file(ids[2], short_tail_damaged);
3559
3560 (set, access, ids)
3561 }
3562
3563 fn evidence_from(result: &VerificationResult) -> HashMap<FileId, Vec<bool>> {
3566 result
3567 .files
3568 .iter()
3569 .map(|file| (file.file_id, file.valid_slices.clone()))
3570 .collect()
3571 }
3572
3573 #[test]
3578 fn absent_evidence_leaves_verification_unchanged() {
3579 let slice_size = 512u64;
3580 let (set, access, ids) = evidence_corpus(slice_size);
3581 let baseline = verify_selected_file_ids_with_options(&set, &access, &ids, &strict_opts());
3582 assert!(baseline.total_missing_blocks > 0, "{baseline:#?}");
3583
3584 let nothing_proven: HashMap<FileId, Vec<bool>> = baseline
3585 .files
3586 .iter()
3587 .map(|file| (file.file_id, vec![false; file.valid_slices.len()]))
3588 .collect();
3589 let wrong_shape: HashMap<FileId, Vec<bool>> = baseline
3590 .files
3591 .iter()
3592 .map(|file| (file.file_id, vec![true; file.valid_slices.len() + 1]))
3593 .collect();
3594
3595 for (label, proven) in [
3596 ("empty map", HashMap::new()),
3597 ("all-false vectors", nothing_proven),
3598 ("wrong-length vectors", wrong_shape),
3599 ] {
3600 let with_evidence =
3601 verify_selected_file_ids_with_options(&set, &access, &ids, &evidence_opts(proven));
3602 assert_eq!(
3603 format!("{with_evidence:?}"),
3604 format!("{baseline:?}"),
3605 "{label} must leave verification unchanged"
3606 );
3607 }
3608 }
3609
3610 #[test]
3613 fn correct_evidence_keeps_the_verdicts_and_reads_fewer_bytes() {
3614 let slice_size = 512u64;
3615 let (set, memory, ids) = evidence_corpus(slice_size);
3616 let access = CountingAccess::new(memory);
3617
3618 let baseline = verify_selected_file_ids_with_options(&set, &access, &ids, &strict_opts());
3619 let baseline_bytes = access.take_bytes_read();
3620 assert!(baseline_bytes > 0, "the unevidenced pass reads the corpus");
3621
3622 let evidenced = verify_selected_file_ids_with_options(
3623 &set,
3624 &access,
3625 &ids,
3626 &evidence_opts(evidence_from(&baseline)),
3627 );
3628 let evidenced_bytes = access.take_bytes_read();
3629
3630 assert_eq!(
3631 format!("{evidenced:?}"),
3632 format!("{baseline:?}"),
3633 "correct evidence must not change a single verdict"
3634 );
3635 assert!(
3636 evidenced_bytes < baseline_bytes,
3637 "evidence must save reads: {evidenced_bytes} vs {baseline_bytes} bytes"
3638 );
3639 }
3640
3641 #[test]
3647 fn damage_in_an_unproven_slice_matches_the_full_read_at_every_position() {
3648 let slice_size = 256u64;
3649 let slice_count = 6usize;
3650 let clean = deterministic_bytes(slice_count * slice_size as usize, 9);
3651 let (set, _, ids) = setup_test_set_multi(&[(&clean, "walked.bin")], slice_size);
3652 let file_id = ids[0];
3653
3654 for damaged_index in 0..slice_count {
3655 let mut damaged = clean.clone();
3656 let start = damaged_index * slice_size as usize;
3657 damaged[start..start + slice_size as usize].fill(0x5A);
3658 let mut memory = MemoryFileAccess::new();
3659 memory.add_file(file_id, damaged);
3660 let access = CountingAccess::new(memory);
3661
3662 let baseline =
3663 verify_selected_file_ids_with_options(&set, &access, &ids, &strict_opts());
3664 let baseline_bytes = access.take_bytes_read();
3665 assert_eq!(
3666 baseline.files[0].missing_slice_count, 1,
3667 "slice {damaged_index} must read as damaged"
3668 );
3669
3670 let evidenced = verify_selected_file_ids_with_options(
3674 &set,
3675 &access,
3676 &ids,
3677 &evidence_opts(evidence_from(&baseline)),
3678 );
3679 let evidenced_bytes = access.take_bytes_read();
3680
3681 assert_eq!(
3682 format!("{evidenced:?}"),
3683 format!("{baseline:?}"),
3684 "slice {damaged_index}: the evidenced result must match the full read exactly"
3685 );
3686 assert_eq!(
3687 evidenced_bytes, slice_size as usize,
3688 "slice {damaged_index}: only the unproven slice should be read \
3689 (full read was {baseline_bytes} bytes)"
3690 );
3691 }
3692 }
3693
3694 #[test]
3699 fn evidence_cannot_resurrect_a_missing_file() {
3700 let slice_size = 512u64;
3701 let data = deterministic_bytes(3 * slice_size as usize, 5);
3702 let (set, _, ids) = setup_test_set_multi(&[(&data, "gone.bin")], slice_size);
3703 let access = CountingAccess::new(MemoryFileAccess::new());
3704
3705 let baseline = verify_selected_file_ids_with_options(&set, &access, &ids, &strict_opts());
3706 let proven: HashMap<FileId, Vec<bool>> = [(ids[0], vec![true; 3])].into_iter().collect();
3707 let evidenced =
3708 verify_selected_file_ids_with_options(&set, &access, &ids, &evidence_opts(proven));
3709
3710 assert!(matches!(evidenced.files[0].status, FileStatus::Missing));
3711 assert_eq!(evidenced.files[0].valid_slices, vec![false; 3]);
3712 assert_eq!(evidenced.total_missing_blocks, 3);
3713 assert_eq!(format!("{evidenced:?}"), format!("{baseline:?}"));
3714 assert_eq!(access.take_bytes_read(), 0);
3715 }
3716
3717 #[test]
3722 fn evidence_for_a_length_mismatched_file_is_ignored() {
3723 let slice_size = 512u64;
3724 let data = deterministic_bytes(3 * slice_size as usize, 6);
3725 let (set, _, ids) = setup_test_set_multi(&[(&data, "truncated.bin")], slice_size);
3726 let mut memory = MemoryFileAccess::new();
3727 memory.add_file(ids[0], data[..data.len() - 100].to_vec());
3728 let access = CountingAccess::new(memory);
3729
3730 let baseline = verify_selected_file_ids_with_options(&set, &access, &ids, &strict_opts());
3731 let proven: HashMap<FileId, Vec<bool>> = [(ids[0], vec![true; 3])].into_iter().collect();
3732 let evidenced =
3733 verify_selected_file_ids_with_options(&set, &access, &ids, &evidence_opts(proven));
3734
3735 assert!(matches!(baseline.files[0].status, FileStatus::Damaged(_)));
3736 assert_eq!(
3737 format!("{evidenced:?}"),
3738 format!("{baseline:?}"),
3739 "a length mismatch must discard the evidence, not believe it"
3740 );
3741 }
3742
3743 #[test]
3748 fn a_fully_proven_file_is_complete_without_a_read() {
3749 let slice_size = 512u64;
3750 let data = deterministic_bytes(4 * slice_size as usize, 7);
3751 let (set, memory, ids) = setup_test_set_multi(&[(&data, "proven.bin")], slice_size);
3752 let access = CountingAccess::new(memory);
3753
3754 let proven: HashMap<FileId, Vec<bool>> = [(ids[0], vec![true; 4])].into_iter().collect();
3755 let evidenced =
3756 verify_selected_file_ids_with_options(&set, &access, &ids, &evidence_opts(proven));
3757
3758 assert!(matches!(evidenced.files[0].status, FileStatus::Complete));
3759 assert_eq!(evidenced.files[0].valid_slices, vec![true; 4]);
3760 assert_eq!(evidenced.total_missing_blocks, 0);
3761 assert!(matches!(evidenced.repairable, Repairability::NotNeeded));
3762 assert_eq!(
3763 access.take_bytes_read(),
3764 0,
3765 "a fully proven file must cost no reads"
3766 );
3767 }
3768
3769 #[test]
3774 fn evidence_composes_with_fast_verify_and_the_parallel_driver() {
3775 let slice_size = 512u64;
3776 let (set, memory, ids) = evidence_corpus(slice_size);
3777 let baseline = verify_selected_file_ids_with_options(&set, &memory, &ids, &strict_opts());
3778 let proven = evidence_from(&baseline);
3779
3780 let mut fast_with_evidence = fast_opts();
3781 fast_with_evidence.proven_slices = proven.clone();
3782 let fast = verify_selected_file_ids_with_options(&set, &memory, &ids, &fast_with_evidence);
3783 assert_eq!(
3784 format!("{fast:?}"),
3785 format!("{baseline:?}"),
3786 "evidence under fast verify must reach the same verdicts"
3787 );
3788
3789 let access = CountingAccess::new(memory);
3790 let parallel_baseline =
3791 verify_selected_file_ids_parallel_with_options(&set, &access, &ids, &strict_opts());
3792 let parallel_bytes = access.take_bytes_read();
3793 let parallel = verify_selected_file_ids_parallel_with_options(
3794 &set,
3795 &access,
3796 &ids,
3797 &evidence_opts(proven),
3798 );
3799 let parallel_evidenced_bytes = access.take_bytes_read();
3800 assert_eq!(
3801 format!("{parallel:?}"),
3802 format!("{parallel_baseline:?}"),
3803 "the parallel driver must honour evidence without changing verdicts"
3804 );
3805 assert!(
3806 parallel_evidenced_bytes < parallel_bytes,
3807 "the parallel driver must also save reads: \
3808 {parallel_evidenced_bytes} vs {parallel_bytes}"
3809 );
3810 }
3811
3812 #[test]
3816 fn the_plan_names_exactly_the_unproven_slices() {
3817 let slice_size = 512u64;
3818 let data = deterministic_bytes(6 * slice_size as usize, 11);
3819 let (set, access, ids) = setup_test_set_multi(&[(&data, "planned.bin")], slice_size);
3820 let file_id = ids[0];
3821
3822 let full = sliced_verify_plan(&set, &access, &file_id, None).expect("plan");
3823 match full.work {
3824 SlicedVerifyWork::Spans(spans) => {
3825 let covered: usize = spans.iter().map(|(_, count)| count).sum();
3826 assert_eq!(covered, 6, "spans must tile every slice: {spans:?}");
3827 assert_eq!(spans[0].0, 0);
3828 }
3829 SlicedVerifyWork::Slices(_) => panic!("a plan with no evidence reads spans"),
3830 }
3831 assert!(full.proven.is_none());
3832
3833 for mask in [
3834 vec![false; 6],
3835 vec![true, false, true, false, true, false],
3836 vec![true, true, false, false, false, true],
3837 vec![true; 6],
3838 ] {
3839 let plan = sliced_verify_plan(&set, &access, &file_id, Some(&mask)).expect("plan");
3840 let expected: Vec<usize> = mask
3841 .iter()
3842 .enumerate()
3843 .filter_map(|(index, proven)| (!*proven).then_some(index))
3844 .collect();
3845 match plan.work {
3846 SlicedVerifyWork::Slices(indices) => assert_eq!(indices, expected, "{mask:?}"),
3847 SlicedVerifyWork::Spans(_) => panic!("an evidenced plan reads named slices"),
3848 }
3849 assert_eq!(plan.proven.as_deref(), Some(mask.as_slice()));
3850 }
3851 }
3852
3853 #[test]
3858 fn a_failed_read_fails_only_its_own_slice_in_a_batch() {
3859 let slice_size = 512u64;
3860 let slice_count = 6usize;
3861 let data = deterministic_bytes(slice_count * slice_size as usize, 13);
3862 let (set, _, ids) = setup_test_set_multi(&[(&data, "cut-short.bin")], slice_size);
3863 let file_id = ids[0];
3864
3865 struct ShortAccess {
3868 data: Vec<u8>,
3869 reported_len: u64,
3870 file_id: FileId,
3871 }
3872 impl FileAccess for ShortAccess {
3873 fn read_file_range(
3874 &self,
3875 _file_id: &FileId,
3876 offset: u64,
3877 len: u64,
3878 ) -> io::Result<Vec<u8>> {
3879 let start = (offset as usize).min(self.data.len());
3880 let end = (start + len as usize).min(self.data.len());
3881 Ok(self.data[start..end].to_vec())
3882 }
3883 fn read_file_range_into(
3884 &self,
3885 _file_id: &FileId,
3886 offset: u64,
3887 dst: &mut [u8],
3888 ) -> io::Result<usize> {
3889 let start = (offset as usize).min(self.data.len());
3890 let end = (start + dst.len()).min(self.data.len());
3891 dst[..end - start].copy_from_slice(&self.data[start..end]);
3892 Ok(end - start)
3893 }
3894 fn file_exists(&self, file_id: &FileId) -> bool {
3895 *file_id == self.file_id
3896 }
3897 fn file_length(&self, _file_id: &FileId) -> Option<u64> {
3898 Some(self.reported_len)
3899 }
3900 fn read_file(&self, _file_id: &FileId) -> io::Result<Vec<u8>> {
3901 Ok(self.data.clone())
3902 }
3903 fn write_file_range(
3904 &mut self,
3905 _file_id: &FileId,
3906 _offset: u64,
3907 _data: &[u8],
3908 ) -> io::Result<()> {
3909 Err(io::Error::new(io::ErrorKind::Unsupported, "read-only"))
3910 }
3911 }
3912
3913 let access = ShortAccess {
3914 data: data[..4 * slice_size as usize].to_vec(),
3915 reported_len: data.len() as u64,
3916 file_id,
3917 };
3918 let proven: HashMap<FileId, Vec<bool>> =
3919 [(file_id, vec![false; slice_count])].into_iter().collect();
3920 let result =
3921 verify_selected_file_ids_with_options(&set, &access, &ids, &evidence_opts(proven));
3922
3923 assert_eq!(
3924 result.files[0].valid_slices,
3925 vec![true, true, true, true, false, false],
3926 "a short read must condemn only the slices it actually cut short"
3927 );
3928 assert_eq!(result.files[0].missing_slice_count, 2);
3929 }
3930}