1use std::collections::{BTreeMap, HashMap};
8use std::sync::Arc;
9use std::sync::atomic::{AtomicUsize, Ordering};
10
11use thiserror::Error;
12
13use crate::checksum::SliceChecksumState;
14use crate::packet::Packet;
15use crate::par2_set::Par2FileSet;
16use crate::types::{FileId, RecoverySetId, SliceChecksum};
17use crate::verify::{FileStatus, FileVerification, Repairability, VerificationResult};
18
19const DEFAULT_BUFFERED_BOUNDARY_BYTES: usize = 64 * 1024 * 1024;
20const MAX_PARTIAL_RANGES_PER_SLICE: usize = 4096;
21const PARTIAL_RANGE_ACCOUNTING_BYTES: usize = std::mem::size_of::<(u64, Vec<u8>)>() + 64;
22
23#[derive(Clone, Debug)]
29pub struct VerificationMemoryBudget {
30 inner: Arc<VerificationMemoryBudgetInner>,
31}
32
33#[derive(Debug)]
34struct VerificationMemoryBudgetInner {
35 max_buffered_bytes: usize,
36 buffered_bytes: AtomicUsize,
37}
38
39impl VerificationMemoryBudget {
40 pub fn new(max_buffered_bytes: usize) -> Self {
42 Self {
43 inner: Arc::new(VerificationMemoryBudgetInner {
44 max_buffered_bytes,
45 buffered_bytes: AtomicUsize::new(0),
46 }),
47 }
48 }
49
50 pub fn max_buffered_bytes(&self) -> usize {
52 self.inner.max_buffered_bytes
53 }
54
55 pub fn buffered_bytes(&self) -> usize {
57 self.inner.buffered_bytes.load(Ordering::Acquire)
58 }
59
60 pub fn available_bytes(&self) -> usize {
62 self.max_buffered_bytes()
63 .saturating_sub(self.buffered_bytes())
64 }
65
66 fn try_reserve(&self, bytes: usize) -> bool {
67 if bytes == 0 {
68 return true;
69 }
70
71 let limit = self.inner.max_buffered_bytes;
72 let mut current = self.inner.buffered_bytes.load(Ordering::Acquire);
73 loop {
74 if bytes > limit.saturating_sub(current) {
75 return false;
76 }
77
78 match self.inner.buffered_bytes.compare_exchange_weak(
79 current,
80 current + bytes,
81 Ordering::AcqRel,
82 Ordering::Acquire,
83 ) {
84 Ok(_) => return true,
85 Err(observed) => current = observed,
86 }
87 }
88 }
89
90 fn release(&self, bytes: usize) {
91 if bytes != 0 {
92 self.inner.buffered_bytes.fetch_sub(bytes, Ordering::AcqRel);
93 }
94 }
95}
96
97impl Default for VerificationMemoryBudget {
98 fn default() -> Self {
99 Self::new(DEFAULT_BUFFERED_BOUNDARY_BYTES)
100 }
101}
102
103#[derive(Clone, Debug, Default)]
105pub struct VerificationSessionOptions {
106 memory_budget: VerificationMemoryBudget,
107}
108
109impl VerificationSessionOptions {
110 pub fn new() -> Self {
112 Self::default()
113 }
114
115 pub fn with_memory_budget(mut self, memory_budget: VerificationMemoryBudget) -> Self {
117 self.memory_budget = memory_budget;
118 self
119 }
120
121 pub fn memory_budget(&self) -> &VerificationMemoryBudget {
123 &self.memory_budget
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum SliceEvidenceStrength {
133 Crc32Only,
135 Crc32AndMd5,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
141pub enum InStreamCrc32ProofError {
142 #[error("in-stream CRC32 covered no bytes")]
143 EmptyCoverage,
144 #[error("in-stream CRC32 did not cover the whole slice")]
145 IncompleteSliceCoverage,
146 #[error("in-stream CRC32 was not derived from the bytes the source will serve")]
147 UnverifiedSourceBytes,
148 #[error("in-stream CRC32 has no independent second-grid CRC32 coverage")]
149 NoIndependentCrc32Coverage,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct InStreamCrc32Proof {
182 covered_length: u64,
183}
184
185impl InStreamCrc32Proof {
186 pub fn try_new(
193 covered_length: u64,
194 slice_fully_covered: bool,
195 derived_from_durable_bytes: bool,
196 independently_crc32_covered: bool,
197 ) -> Result<Self, InStreamCrc32ProofError> {
198 if covered_length == 0 {
199 return Err(InStreamCrc32ProofError::EmptyCoverage);
200 }
201 if !slice_fully_covered {
202 return Err(InStreamCrc32ProofError::IncompleteSliceCoverage);
203 }
204 if !derived_from_durable_bytes {
205 return Err(InStreamCrc32ProofError::UnverifiedSourceBytes);
206 }
207 if !independently_crc32_covered {
208 return Err(InStreamCrc32ProofError::NoIndependentCrc32Coverage);
209 }
210
211 Ok(Self { covered_length })
212 }
213
214 pub fn covered_length(&self) -> u64 {
216 self.covered_length
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub struct SliceEvidence {
231 recovery_set_id: RecoverySetId,
232 file_id: FileId,
233 slice_index: u32,
234 valid: bool,
235 strength: SliceEvidenceStrength,
236 in_stream: Option<InStreamCrc32Proof>,
239}
240
241impl SliceEvidence {
242 pub fn recovery_set_id(&self) -> RecoverySetId {
244 self.recovery_set_id
245 }
246
247 pub fn file_id(&self) -> FileId {
249 self.file_id
250 }
251
252 pub fn slice_index(&self) -> u32 {
254 self.slice_index
255 }
256
257 pub fn is_valid(&self) -> bool {
259 self.valid
260 }
261
262 pub fn strength(&self) -> SliceEvidenceStrength {
269 self.strength
270 }
271
272 pub fn from_in_stream_crc32(
312 recovery_set_id: RecoverySetId,
313 file_id: FileId,
314 slice_index: u32,
315 valid: bool,
316 proof: InStreamCrc32Proof,
317 ) -> Self {
318 Self {
319 recovery_set_id,
320 file_id,
321 slice_index,
322 valid,
323 strength: SliceEvidenceStrength::Crc32Only,
324 in_stream: Some(proof),
325 }
326 }
327
328 pub fn in_stream_proof(&self) -> Option<&InStreamCrc32Proof> {
331 self.in_stream.as_ref()
332 }
333
334 pub fn may_seed_repair_input(&self) -> bool {
343 self.strength == SliceEvidenceStrength::Crc32AndMd5 || self.in_stream.is_some()
344 }
345
346 #[cfg(test)]
347 pub(crate) fn for_test(
348 recovery_set_id: RecoverySetId,
349 file_id: FileId,
350 slice_index: u32,
351 valid: bool,
352 strength: SliceEvidenceStrength,
353 ) -> Self {
354 Self {
355 recovery_set_id,
356 file_id,
357 slice_index,
358 valid,
359 strength,
360 in_stream: None,
361 }
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct SettleRead {
369 file_id: FileId,
370 slice_index: u32,
371 offset: u64,
372 length: u64,
373}
374
375impl SettleRead {
376 fn new(file_id: FileId, slice_index: usize, offset: u64, length: u64) -> Self {
377 Self {
378 file_id,
379 slice_index: u32::try_from(slice_index).unwrap_or(u32::MAX),
380 offset,
381 length,
382 }
383 }
384
385 pub fn file_id(&self) -> FileId {
387 self.file_id
388 }
389
390 pub fn slice_index(&self) -> u32 {
392 self.slice_index
393 }
394
395 pub fn offset(&self) -> u64 {
397 self.offset
398 }
399
400 pub fn length(&self) -> u64 {
402 self.length
403 }
404}
405
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub enum FeedDisposition {
409 Verified,
411 Buffered,
413 Duplicate,
415 MetadataPending,
417 UnknownFile,
419 OutOfRange,
421 BudgetExhausted,
423 ConflictingOverlap,
425 NeedsSettleRead,
428}
429
430#[derive(Debug, Clone, PartialEq, Eq)]
432pub struct FeedOutcome {
433 disposition: FeedDisposition,
434 evidence: Vec<SliceEvidence>,
435 settle_reads: Vec<SettleRead>,
436}
437
438impl FeedOutcome {
439 fn new(disposition: FeedDisposition) -> Self {
440 Self {
441 disposition,
442 evidence: Vec::new(),
443 settle_reads: Vec::new(),
444 }
445 }
446
447 pub fn disposition(&self) -> FeedDisposition {
449 self.disposition
450 }
451
452 pub fn evidence(&self) -> &[SliceEvidence] {
454 &self.evidence
455 }
456
457 pub fn settle_reads(&self) -> &[SettleRead] {
459 &self.settle_reads
460 }
461}
462
463pub type FeedRangeOutcome = FeedOutcome;
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467struct SliceFingerprint {
468 crc32: u32,
469 md5: [u8; 16],
470}
471
472impl SliceFingerprint {
473 fn from_data(data: &[u8], slice_size: u64) -> Self {
474 let mut state = SliceChecksumState::new();
475 state.update(data);
476 let pad_to = ((data.len() as u64) < slice_size).then_some(slice_size);
477 let (crc32, md5) = state.finalize(pad_to);
478 Self { crc32, md5 }
479 }
480
481 fn is_valid_for(self, expected: &SliceChecksum) -> bool {
482 self.crc32 == expected.crc32 && self.md5 == expected.md5
483 }
484}
485
486struct PartialSlice {
488 expected_len: u64,
489 ranges: BTreeMap<u64, Vec<u8>>,
490 buffered_bytes: usize,
491 reserved_bytes: usize,
492}
493
494impl PartialSlice {
495 fn new(expected_len: u64) -> Self {
496 Self {
497 expected_len,
498 ranges: BTreeMap::new(),
499 buffered_bytes: 0,
500 reserved_bytes: 0,
501 }
502 }
503
504 fn matches(&self, start: u64, data: &[u8]) -> bool {
505 let end = start + data.len() as u64;
506 self.ranges.range(..end).all(|(&range_start, existing)| {
507 let range_end = range_start + existing.len() as u64;
508 let overlap_start = range_start.max(start);
509 let overlap_end = range_end.min(end);
510 overlap_start >= overlap_end
511 || existing
512 [(overlap_start - range_start) as usize..(overlap_end - range_start) as usize]
513 == data[(overlap_start - start) as usize..(overlap_end - start) as usize]
514 })
515 }
516
517 fn uncovered_ranges(&self, start: u64, end: u64) -> Vec<(u64, u64)> {
518 let mut cursor = start;
519 let mut uncovered = Vec::new();
520
521 for (&range_start, existing) in self.ranges.range(..end) {
522 let range_end = range_start + existing.len() as u64;
523 if range_end <= cursor {
524 continue;
525 }
526 if range_start > cursor {
527 let gap_end = range_start.min(end);
528 uncovered.push((cursor, gap_end));
529 cursor = gap_end;
530 }
531 cursor = cursor.max(range_end.min(end));
532 if cursor == end {
533 break;
534 }
535 }
536
537 if cursor < end {
538 uncovered.push((cursor, end));
539 }
540 uncovered
541 }
542
543 fn insert(
546 &mut self,
547 start: u64,
548 data: &[u8],
549 budget: &VerificationMemoryBudget,
550 ) -> Option<usize> {
551 let end = start + data.len() as u64;
552 let uncovered = self.uncovered_ranges(start, end);
553 if self.ranges.len().saturating_add(uncovered.len()) > MAX_PARTIAL_RANGES_PER_SLICE {
554 return None;
555 }
556 let added = uncovered
557 .iter()
558 .map(|(from, to)| usize::try_from(to - from).unwrap_or(usize::MAX))
559 .sum::<usize>();
560 let reservation = added.saturating_add(
561 uncovered
562 .len()
563 .saturating_mul(PARTIAL_RANGE_ACCOUNTING_BYTES),
564 );
565
566 if !budget.try_reserve(reservation) {
567 return None;
568 }
569
570 for (from, to) in uncovered {
571 self.ranges.insert(
572 from,
573 data[(from - start) as usize..(to - start) as usize].to_vec(),
574 );
575 }
576 self.buffered_bytes += added;
577 self.reserved_bytes = self.reserved_bytes.saturating_add(reservation);
578 Some(added)
579 }
580
581 fn is_complete(&self) -> bool {
582 self.buffered_bytes as u64 == self.expected_len
583 }
584
585 fn fingerprint(&self, slice_size: u64) -> Option<SliceFingerprint> {
586 if !self.is_complete() {
587 return None;
588 }
589
590 let mut state = SliceChecksumState::new();
591 let mut offset = 0;
592 for (&range_start, data) in &self.ranges {
593 if range_start != offset {
594 return None;
595 }
596 state.update(data);
597 offset += data.len() as u64;
598 }
599 if offset != self.expected_len {
600 return None;
601 }
602
603 let pad_to = (self.expected_len < slice_size).then_some(slice_size);
604 let (crc32, md5) = state.finalize(pad_to);
605 Some(SliceFingerprint { crc32, md5 })
606 }
607
608 fn settle_reads(
609 &self,
610 file_id: FileId,
611 slice_index: usize,
612 slice_offset: u64,
613 ) -> Vec<SettleRead> {
614 self.uncovered_ranges(0, self.expected_len)
615 .into_iter()
616 .map(|(start, end)| {
617 SettleRead::new(file_id, slice_index, slice_offset + start, end - start)
618 })
619 .collect()
620 }
621}
622
623struct FileVerificationState {
625 partial_slices: HashMap<usize, PartialSlice>,
627 verified_slices: Vec<Option<bool>>,
630 fingerprints: Vec<Option<SliceFingerprint>>,
633 bytes_received: u64,
635 file_length: u64,
637 slice_size: u64,
639}
640
641impl FileVerificationState {
642 fn new(file_length: u64, slice_size: u64) -> Self {
643 let num_slices = if file_length == 0 || slice_size == 0 {
644 0
645 } else {
646 file_length.div_ceil(slice_size) as usize
647 };
648
649 Self {
650 partial_slices: HashMap::new(),
651 verified_slices: vec![None; num_slices],
652 fingerprints: vec![None; num_slices],
653 bytes_received: 0,
654 file_length,
655 slice_size,
656 }
657 }
658
659 fn slice_offset(&self, slice_index: usize) -> Option<u64> {
660 (slice_index as u64).checked_mul(self.slice_size)
661 }
662
663 fn slice_len(&self, slice_index: usize) -> Option<u64> {
664 let offset = self.slice_offset(slice_index)?;
665 (offset < self.file_length).then(|| (self.file_length - offset).min(self.slice_size))
666 }
667
668 fn evidence(
669 &self,
670 recovery_set_id: RecoverySetId,
671 file_id: FileId,
672 slice_index: usize,
673 valid: bool,
674 ) -> SliceEvidence {
675 SliceEvidence {
676 recovery_set_id,
677 file_id,
678 slice_index: u32::try_from(slice_index).unwrap_or(u32::MAX),
679 valid,
680 strength: if self
681 .fingerprints
682 .get(slice_index)
683 .is_some_and(Option::is_some)
684 {
685 SliceEvidenceStrength::Crc32AndMd5
686 } else {
687 SliceEvidenceStrength::Crc32Only
688 },
689 in_stream: None,
692 }
693 }
694
695 fn full_slice_read(&self, file_id: FileId, slice_index: usize) -> SettleRead {
696 SettleRead::new(
697 file_id,
698 slice_index,
699 self.slice_offset(slice_index).unwrap_or(self.file_length),
700 self.slice_len(slice_index).unwrap_or(0),
701 )
702 }
703
704 fn discard_partial(&mut self, slice_index: usize, budget: &VerificationMemoryBudget) -> usize {
705 self.partial_slices
706 .remove(&slice_index)
707 .map(|partial| {
708 budget.release(partial.reserved_bytes);
709 partial.buffered_bytes
710 })
711 .unwrap_or(0)
712 }
713
714 fn verified_count(&self) -> usize {
716 self.verified_slices
717 .iter()
718 .filter(|v| **v == Some(true))
719 .count()
720 }
721
722 fn damaged_count(&self) -> usize {
724 self.verified_slices
725 .iter()
726 .filter(|v| **v == Some(false))
727 .count()
728 }
729
730 fn pending_count(&self) -> usize {
732 self.verified_slices.iter().filter(|v| v.is_none()).count()
733 }
734
735 fn total_slices(&self) -> usize {
737 self.verified_slices.len()
738 }
739}
740
741enum SliceFeed {
742 Verified(SliceEvidence),
743 Buffered(Vec<SettleRead>),
744 Duplicate(Vec<SettleRead>),
745 BudgetExhausted(SettleRead),
746 Conflict(SettleRead),
747 NeedsSettleRead(SettleRead),
748}
749
750pub struct VerificationSession {
760 par2_set: Option<Arc<Par2FileSet>>,
761 file_states: HashMap<FileId, FileVerificationState>,
762 buffered_packets: Vec<Packet>,
765 memory_budget: VerificationMemoryBudget,
766}
767
768impl VerificationSession {
769 pub fn new() -> Self {
771 Self::with_options(VerificationSessionOptions::default())
772 }
773
774 pub fn with_options(options: VerificationSessionOptions) -> Self {
776 Self {
777 par2_set: None,
778 file_states: HashMap::new(),
779 buffered_packets: Vec::new(),
780 memory_budget: options.memory_budget,
781 }
782 }
783
784 pub fn with_memory_budget(memory_budget: VerificationMemoryBudget) -> Self {
786 Self::with_options(VerificationSessionOptions::new().with_memory_budget(memory_budget))
787 }
788
789 pub fn memory_budget(&self) -> &VerificationMemoryBudget {
791 &self.memory_budget
792 }
793
794 fn initialize_file_states(&mut self) {
795 let Some(par2_set) = self.par2_set.as_ref() else {
796 return;
797 };
798
799 for file_id in &par2_set.recovery_file_ids {
800 if !self.file_states.contains_key(file_id)
801 && let Some(desc) = par2_set.file_description(file_id)
802 {
803 self.file_states.insert(
804 *file_id,
805 FileVerificationState::new(desc.length, par2_set.slice_size),
806 );
807 }
808 }
809 }
810
811 pub fn add_par2_data(&mut self, packets: &[Packet]) {
817 if let Some(par2_set) = self.par2_set.as_mut() {
818 let _ = Arc::make_mut(par2_set).merge_packets(packets.to_vec());
822 self.initialize_file_states();
823 return;
824 }
825
826 self.buffered_packets.extend(packets.iter().cloned());
827
828 match Par2FileSet::from_packets(self.buffered_packets.clone()) {
830 Ok(set) => {
831 self.par2_set = Some(Arc::new(set));
832 self.buffered_packets.clear();
835 self.initialize_file_states();
836 }
837 Err(_) => {
838 }
840 }
841 }
842
843 pub fn feed_data(&mut self, file_id: &FileId, offset: u64, data: &[u8]) {
853 let _ = self.feed_range(file_id, offset, data);
854 }
855
856 pub fn feed_range(&mut self, file_id: &FileId, offset: u64, data: &[u8]) -> FeedOutcome {
863 let par2_set = match &self.par2_set {
864 Some(set) => Arc::clone(set),
865 None => return FeedOutcome::new(FeedDisposition::MetadataPending),
866 };
867
868 let Some(desc) = par2_set.file_description(file_id) else {
869 return FeedOutcome::new(FeedDisposition::UnknownFile);
870 };
871 let Some(checksums) = par2_set.file_checksums(file_id) else {
872 return FeedOutcome::new(FeedDisposition::MetadataPending);
873 };
874 if par2_set.slice_size == 0 {
875 return FeedOutcome::new(FeedDisposition::OutOfRange);
876 }
877
878 let range_end = match offset.checked_add(data.len() as u64) {
879 Some(end) if offset <= desc.length && end <= desc.length => end,
880 _ => return FeedOutcome::new(FeedDisposition::OutOfRange),
881 };
882 if data.is_empty() {
883 return FeedOutcome::new(FeedDisposition::Duplicate);
884 }
885
886 if !self.file_states.contains_key(file_id) {
889 self.file_states.insert(
890 *file_id,
891 FileVerificationState::new(desc.length, par2_set.slice_size),
892 );
893 }
894
895 let first_slice = usize::try_from(offset / par2_set.slice_size)
896 .map_err(|_| ())
897 .ok();
898 let last_slice = usize::try_from((range_end - 1) / par2_set.slice_size)
899 .map_err(|_| ())
900 .ok();
901 let (Some(first_slice), Some(last_slice)) = (first_slice, last_slice) else {
902 return FeedOutcome::new(FeedDisposition::OutOfRange);
903 };
904
905 let state = self
906 .file_states
907 .get_mut(file_id)
908 .expect("state inserted above");
909 let mut outcome = FeedOutcome::new(FeedDisposition::Duplicate);
910 let mut saw_verified = false;
911 let mut saw_buffered = false;
912 let mut saw_budget_exhausted = false;
913 let mut saw_conflict = false;
914 let mut saw_needs_settle_read = false;
915
916 for slice_index in first_slice..=last_slice {
917 let Some(slice_offset) = state.slice_offset(slice_index) else {
918 saw_needs_settle_read = true;
919 continue;
920 };
921 let Some(slice_len) = state.slice_len(slice_index) else {
922 saw_needs_settle_read = true;
923 continue;
924 };
925 let Some(expected) = checksums.get(slice_index) else {
926 return FeedOutcome::new(FeedDisposition::MetadataPending);
927 };
928
929 let piece_start = offset.max(slice_offset);
930 let piece_end = range_end.min(slice_offset + slice_len);
931 let piece = &data[(piece_start - offset) as usize..(piece_end - offset) as usize];
932 let whole_slice = piece_start == slice_offset && piece.len() as u64 == slice_len;
933
934 match Self::feed_slice_range(
935 state,
936 &self.memory_budget,
937 par2_set.recovery_set_id,
938 *file_id,
939 slice_index,
940 piece_start - slice_offset,
941 piece,
942 whole_slice,
943 expected,
944 ) {
945 SliceFeed::Verified(evidence) => {
946 saw_verified = true;
947 outcome.evidence.push(evidence);
948 }
949 SliceFeed::Buffered(reads) => {
950 saw_buffered = true;
951 Self::append_settle_reads(&mut outcome.settle_reads, reads);
952 }
953 SliceFeed::Duplicate(reads) => {
954 Self::append_settle_reads(&mut outcome.settle_reads, reads);
955 }
956 SliceFeed::BudgetExhausted(read) => {
957 saw_budget_exhausted = true;
958 Self::append_settle_reads(&mut outcome.settle_reads, [read]);
959 }
960 SliceFeed::Conflict(read) => {
961 saw_conflict = true;
962 Self::append_settle_reads(&mut outcome.settle_reads, [read]);
963 }
964 SliceFeed::NeedsSettleRead(read) => {
965 saw_needs_settle_read = true;
966 Self::append_settle_reads(&mut outcome.settle_reads, [read]);
967 }
968 }
969 }
970
971 outcome.disposition = if saw_conflict {
972 FeedDisposition::ConflictingOverlap
973 } else if saw_budget_exhausted {
974 FeedDisposition::BudgetExhausted
975 } else if saw_needs_settle_read {
976 FeedDisposition::NeedsSettleRead
977 } else if saw_verified {
978 FeedDisposition::Verified
979 } else if saw_buffered {
980 FeedDisposition::Buffered
981 } else {
982 FeedDisposition::Duplicate
983 };
984 outcome
985 }
986
987 pub fn feed_data_range(&mut self, file_id: &FileId, offset: u64, data: &[u8]) -> FeedOutcome {
989 self.feed_range(file_id, offset, data)
990 }
991
992 fn append_settle_reads(
993 target: &mut Vec<SettleRead>,
994 reads: impl IntoIterator<Item = SettleRead>,
995 ) {
996 for read in reads {
997 if !target.contains(&read) {
998 target.push(read);
999 }
1000 }
1001 }
1002
1003 #[allow(clippy::too_many_arguments)]
1004 fn feed_slice_range(
1005 state: &mut FileVerificationState,
1006 memory_budget: &VerificationMemoryBudget,
1007 recovery_set_id: RecoverySetId,
1008 file_id: FileId,
1009 slice_index: usize,
1010 slice_data_offset: u64,
1011 data: &[u8],
1012 whole_slice: bool,
1013 expected: &SliceChecksum,
1014 ) -> SliceFeed {
1015 let full_read = state.full_slice_read(file_id, slice_index);
1016
1017 if let Some(fingerprint) = state.fingerprints[slice_index] {
1018 if whole_slice {
1019 return if fingerprint == SliceFingerprint::from_data(data, state.slice_size) {
1020 SliceFeed::Duplicate(Vec::new())
1021 } else {
1022 SliceFeed::Conflict(full_read.clone())
1023 };
1024 }
1025 return SliceFeed::NeedsSettleRead(full_read.clone());
1026 }
1027 if state.verified_slices[slice_index].is_some() {
1028 return SliceFeed::NeedsSettleRead(full_read.clone());
1032 }
1033
1034 let slice_len = state.slice_len(slice_index).unwrap_or(0);
1035 if whole_slice {
1036 let partial_bytes = match state.partial_slices.get(&slice_index) {
1037 Some(partial) if !partial.matches(0, data) => {
1038 state.discard_partial(slice_index, memory_budget);
1039 return SliceFeed::Conflict(full_read.clone());
1040 }
1041 Some(partial) => partial.buffered_bytes as u64,
1042 None => 0,
1043 };
1044 if partial_bytes != 0 {
1045 state.discard_partial(slice_index, memory_budget);
1046 }
1047
1048 let fingerprint = SliceFingerprint::from_data(data, state.slice_size);
1049 let valid = fingerprint.is_valid_for(expected);
1050 state.verified_slices[slice_index] = Some(valid);
1051 state.fingerprints[slice_index] = Some(fingerprint);
1052 state.bytes_received += slice_len.saturating_sub(partial_bytes);
1053 return SliceFeed::Verified(state.evidence(
1054 recovery_set_id,
1055 file_id,
1056 slice_index,
1057 valid,
1058 ));
1059 }
1060
1061 if state
1062 .partial_slices
1063 .get(&slice_index)
1064 .is_some_and(|partial| !partial.matches(slice_data_offset, data))
1065 {
1066 state.discard_partial(slice_index, memory_budget);
1067 return SliceFeed::Conflict(full_read.clone());
1068 }
1069
1070 let had_partial = state.partial_slices.contains_key(&slice_index);
1071 let inserted = state
1072 .partial_slices
1073 .entry(slice_index)
1074 .or_insert_with(|| PartialSlice::new(slice_len))
1075 .insert(slice_data_offset, data, memory_budget);
1076 let Some(inserted) = inserted else {
1077 if !had_partial {
1078 state.partial_slices.remove(&slice_index);
1079 }
1080 return SliceFeed::BudgetExhausted(full_read.clone());
1081 };
1082 state.bytes_received += inserted as u64;
1083
1084 let complete = state
1085 .partial_slices
1086 .get(&slice_index)
1087 .is_some_and(PartialSlice::is_complete);
1088 if complete {
1089 let partial = state
1090 .partial_slices
1091 .remove(&slice_index)
1092 .expect("complete partial slice exists");
1093 let fingerprint = partial
1094 .fingerprint(state.slice_size)
1095 .expect("complete partial slice covers every byte");
1096 memory_budget.release(partial.reserved_bytes);
1097 let valid = fingerprint.is_valid_for(expected);
1098 state.verified_slices[slice_index] = Some(valid);
1099 state.fingerprints[slice_index] = Some(fingerprint);
1100 return SliceFeed::Verified(state.evidence(
1101 recovery_set_id,
1102 file_id,
1103 slice_index,
1104 valid,
1105 ));
1106 }
1107
1108 let reads = state
1109 .partial_slices
1110 .get(&slice_index)
1111 .expect("partial slice remains when incomplete")
1112 .settle_reads(
1113 file_id,
1114 slice_index,
1115 state.slice_offset(slice_index).unwrap_or(state.file_length),
1116 );
1117 if inserted == 0 {
1118 SliceFeed::Duplicate(reads)
1119 } else {
1120 SliceFeed::Buffered(reads)
1121 }
1122 }
1123
1124 pub fn file_status(&self, file_id: &FileId) -> Option<FileStatus> {
1126 let state = self.file_states.get(file_id)?;
1127
1128 if state.total_slices() == 0 {
1129 return Some(FileStatus::Complete);
1130 }
1131
1132 if state.verified_count() == state.total_slices() {
1134 return Some(FileStatus::Complete);
1135 }
1136
1137 let damaged = state.damaged_count() as u32;
1138 if damaged > 0 {
1139 return Some(FileStatus::Damaged(damaged));
1140 }
1141
1142 if state.pending_count() == state.total_slices() {
1144 return Some(FileStatus::Missing);
1146 }
1147
1148 if state.bytes_received == 0 {
1153 Some(FileStatus::Missing)
1154 } else {
1155 Some(FileStatus::Damaged(0))
1156 }
1157 }
1158
1159 pub fn repairability(&self) -> Repairability {
1164 let par2_set = match &self.par2_set {
1165 Some(set) => set,
1166 None => return Repairability::NotNeeded,
1167 };
1168
1169 let mut total_damaged: u32 = 0;
1170 let mut total_missing: u32 = 0;
1171
1172 for file_id in &par2_set.recovery_file_ids {
1173 if let Some(state) = self.file_states.get(file_id) {
1174 total_damaged += state.damaged_count() as u32;
1175 if state.bytes_received == 0 {
1177 total_missing += state.total_slices() as u32;
1178 }
1179 } else {
1180 if let Some(desc) = par2_set.file_description(file_id) {
1182 total_missing += par2_set.slice_count_for_file(desc.length);
1183 }
1184 }
1185 }
1186
1187 let blocks_needed = total_damaged + total_missing;
1188 let blocks_available = par2_set.recovery_block_count();
1189
1190 if blocks_needed == 0 {
1191 Repairability::NotNeeded
1192 } else if blocks_needed <= blocks_available {
1193 Repairability::Repairable {
1194 blocks_needed,
1195 blocks_available,
1196 }
1197 } else {
1198 Repairability::Insufficient {
1199 blocks_needed,
1200 blocks_available,
1201 deficit: blocks_needed - blocks_available,
1202 }
1203 }
1204 }
1205
1206 pub fn is_complete(&self) -> bool {
1208 let par2_set = match &self.par2_set {
1209 Some(set) => set,
1210 None => return false,
1211 };
1212
1213 for file_id in &par2_set.recovery_file_ids {
1214 match self.file_states.get(file_id) {
1215 Some(state) => {
1216 if state.verified_count() != state.total_slices() {
1217 return false;
1218 }
1219 }
1220 None => return false,
1221 }
1222 }
1223
1224 true
1225 }
1226
1227 pub fn verification_result(&self) -> Option<VerificationResult> {
1232 let par2_set = self.par2_set.as_ref()?;
1233
1234 let mut files = Vec::new();
1235 let mut total_missing_blocks = 0u32;
1236
1237 for file_id in &par2_set.recovery_file_ids {
1238 let desc = match par2_set.file_description(file_id) {
1239 Some(d) => d,
1240 None => continue,
1241 };
1242
1243 let state = match self.file_states.get(file_id) {
1244 Some(s) => s,
1245 None => {
1246 let slice_count = par2_set.slice_count_for_file(desc.length);
1248 total_missing_blocks += slice_count;
1249 files.push(FileVerification {
1250 file_id: *file_id,
1251 filename: desc.filename.clone(),
1252 status: FileStatus::Missing,
1253 valid_slices: vec![false; slice_count as usize],
1254 missing_slice_count: slice_count,
1255 });
1256 continue;
1257 }
1258 };
1259
1260 let valid_slices: Vec<bool> = state
1261 .verified_slices
1262 .iter()
1263 .map(|v| v.unwrap_or(false))
1264 .collect();
1265 let missing_count = valid_slices.iter().filter(|&&v| !v).count() as u32;
1266 total_missing_blocks += missing_count;
1267
1268 let status = if missing_count == 0 {
1269 FileStatus::Complete
1270 } else if state.bytes_received == 0 {
1271 FileStatus::Missing
1272 } else {
1273 FileStatus::Damaged(missing_count)
1274 };
1275
1276 files.push(FileVerification {
1277 file_id: *file_id,
1278 filename: desc.filename.clone(),
1279 status,
1280 valid_slices,
1281 missing_slice_count: missing_count,
1282 });
1283 }
1284
1285 let recovery_blocks_available = par2_set.recovery_block_count();
1286 let repairable = if total_missing_blocks == 0 {
1287 Repairability::NotNeeded
1288 } else if total_missing_blocks <= recovery_blocks_available {
1289 Repairability::Repairable {
1290 blocks_needed: total_missing_blocks,
1291 blocks_available: recovery_blocks_available,
1292 }
1293 } else {
1294 Repairability::Insufficient {
1295 blocks_needed: total_missing_blocks,
1296 blocks_available: recovery_blocks_available,
1297 deficit: total_missing_blocks - recovery_blocks_available,
1298 }
1299 };
1300
1301 Some(VerificationResult {
1302 files,
1303 recovery_blocks_available,
1304 total_missing_blocks,
1305 repairable,
1306 })
1307 }
1308
1309 pub fn verify_from_slice_crcs(
1318 &mut self,
1319 file_id: &FileId,
1320 slice_crcs: &[u32],
1321 ) -> Option<Vec<bool>> {
1322 let par2_set = self.par2_set.as_ref()?;
1323 let checksums = par2_set.file_checksums(file_id)?;
1324
1325 if !self.file_states.contains_key(file_id) {
1327 let desc = par2_set.file_description(file_id)?;
1328 self.file_states.insert(
1329 *file_id,
1330 FileVerificationState::new(desc.length, par2_set.slice_size),
1331 );
1332 }
1333
1334 let state = self.file_states.get_mut(file_id)?;
1335 let mut results = Vec::with_capacity(checksums.len());
1336 for (i, expected) in checksums.iter().enumerate() {
1337 let valid = slice_crcs
1338 .get(i)
1339 .map(|&crc| crc == expected.crc32)
1340 .unwrap_or(false);
1341 if state.verified_slices[i].is_none() {
1343 state.discard_partial(i, &self.memory_budget);
1344 state.verified_slices[i] = Some(valid);
1345 if valid {
1346 state.bytes_received = state.bytes_received.max(1);
1348 }
1349 }
1350 results.push(valid);
1351 }
1352
1353 Some(results)
1354 }
1355
1356 pub fn slice_evidence(&self) -> Vec<SliceEvidence> {
1362 let Some(recovery_set_id) = self.par2_set.as_ref().map(|set| set.recovery_set_id) else {
1363 return Vec::new();
1364 };
1365 let mut evidence = self
1366 .file_states
1367 .iter()
1368 .flat_map(|(file_id, state)| {
1369 state
1370 .verified_slices
1371 .iter()
1372 .enumerate()
1373 .filter_map(|(slice_index, valid)| {
1374 valid.map(|valid| {
1375 state.evidence(recovery_set_id, *file_id, slice_index, valid)
1376 })
1377 })
1378 })
1379 .collect::<Vec<_>>();
1380 evidence.sort_unstable_by(|left, right| {
1381 left.file_id()
1382 .as_bytes()
1383 .cmp(right.file_id().as_bytes())
1384 .then_with(|| left.slice_index().cmp(&right.slice_index()))
1385 });
1386 evidence
1387 }
1388
1389 pub fn par2_set(&self) -> Option<&Arc<Par2FileSet>> {
1391 self.par2_set.as_ref()
1392 }
1393}
1394
1395impl Default for VerificationSession {
1396 fn default() -> Self {
1397 Self::new()
1398 }
1399}
1400
1401impl Drop for VerificationSession {
1402 fn drop(&mut self) {
1403 for state in self.file_states.values() {
1404 for partial in state.partial_slices.values() {
1405 self.memory_budget.release(partial.reserved_bytes);
1406 }
1407 }
1408 }
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use super::*;
1414 use crate::checksum;
1415 use crate::packet::header;
1416 use crate::par2_set::RecoverySlice;
1417 use crate::types::SliceChecksum;
1418 use bytes::Bytes;
1419 use md5::{Digest, Md5};
1420
1421 fn make_full_packet(packet_type: &[u8; 16], body: &[u8], recovery_set_id: [u8; 16]) -> Vec<u8> {
1423 let length = (header::HEADER_SIZE + body.len()) as u64;
1424 let mut hash_input = Vec::new();
1425 hash_input.extend_from_slice(&recovery_set_id);
1426 hash_input.extend_from_slice(packet_type);
1427 hash_input.extend_from_slice(body);
1428 let packet_hash: [u8; 16] = Md5::digest(&hash_input).into();
1429
1430 let mut data = Vec::new();
1431 data.extend_from_slice(header::MAGIC);
1432 data.extend_from_slice(&length.to_le_bytes());
1433 data.extend_from_slice(&packet_hash);
1434 data.extend_from_slice(&recovery_set_id);
1435 data.extend_from_slice(packet_type);
1436 data.extend_from_slice(body);
1437 data
1438 }
1439
1440 fn build_par2_packets(file_data: &[u8], slice_size: u64) -> (Vec<u8>, FileId, [u8; 16]) {
1442 let file_length = file_data.len() as u64;
1443 let hash_full = checksum::md5(file_data);
1444 let hash_16k_data = &file_data[..file_data.len().min(16384)];
1445 let hash_16k = checksum::md5(hash_16k_data);
1446
1447 let filename = b"testfile.dat";
1448 let mut id_input = Vec::new();
1449 id_input.extend_from_slice(&hash_16k);
1450 id_input.extend_from_slice(&file_length.to_le_bytes());
1451 id_input.extend_from_slice(filename);
1452 let file_id_bytes: [u8; 16] = Md5::digest(&id_input).into();
1453 let file_id = FileId::from_bytes(file_id_bytes);
1454
1455 let num_slices = if file_length == 0 {
1456 0
1457 } else {
1458 file_length.div_ceil(slice_size) as usize
1459 };
1460
1461 let mut checksums = Vec::new();
1462 for i in 0..num_slices {
1463 let offset = i as u64 * slice_size;
1464 let end = ((offset + slice_size) as usize).min(file_data.len());
1465 let slice_data = &file_data[offset as usize..end];
1466 let mut state = SliceChecksumState::new();
1467 state.update(slice_data);
1468 let pad_to = if (slice_data.len() as u64) < slice_size {
1469 Some(slice_size)
1470 } else {
1471 None
1472 };
1473 let (crc, md5) = state.finalize(pad_to);
1474 checksums.push(SliceChecksum { crc32: crc, md5 });
1475 }
1476
1477 let mut main_body = Vec::new();
1478 main_body.extend_from_slice(&slice_size.to_le_bytes());
1479 main_body.extend_from_slice(&1u32.to_le_bytes());
1480 main_body.extend_from_slice(&file_id_bytes);
1481 let rsid: [u8; 16] = Md5::digest(&main_body).into();
1482
1483 let mut fd_body = Vec::new();
1484 fd_body.extend_from_slice(&file_id_bytes);
1485 fd_body.extend_from_slice(&hash_full);
1486 fd_body.extend_from_slice(&hash_16k);
1487 fd_body.extend_from_slice(&file_length.to_le_bytes());
1488 fd_body.extend_from_slice(filename);
1489 while fd_body.len() % 4 != 0 {
1490 fd_body.push(0);
1491 }
1492
1493 let mut ifsc_body = Vec::new();
1494 ifsc_body.extend_from_slice(&file_id_bytes);
1495 for cs in &checksums {
1496 ifsc_body.extend_from_slice(&cs.md5);
1497 ifsc_body.extend_from_slice(&cs.crc32.to_le_bytes());
1498 }
1499
1500 let mut stream = Vec::new();
1501 stream.extend_from_slice(&make_full_packet(header::TYPE_MAIN, &main_body, rsid));
1502 stream.extend_from_slice(&make_full_packet(header::TYPE_FILE_DESC, &fd_body, rsid));
1503 stream.extend_from_slice(&make_full_packet(header::TYPE_IFSC, &ifsc_body, rsid));
1504
1505 (stream, file_id, rsid)
1506 }
1507
1508 fn parse_packets(data: &[u8]) -> Vec<Packet> {
1509 crate::packet::scan_packets(data, 0)
1510 .unwrap()
1511 .into_iter()
1512 .map(|(p, _)| p)
1513 .collect()
1514 }
1515
1516 #[test]
1517 fn session_feed_correct_data_all_pass() {
1518 let slice_size = 1024u64;
1519 let file_data: Vec<u8> = (0..2048u32).map(|i| (i % 256) as u8).collect();
1520 let (par2_bytes, file_id, _rsid) = build_par2_packets(&file_data, slice_size);
1521
1522 let mut session = VerificationSession::new();
1523
1524 let packets = parse_packets(&par2_bytes);
1526 session.add_par2_data(&packets);
1527 assert!(session.par2_set().is_some());
1528
1529 session.feed_data(&file_id, 0, &file_data[0..1024]);
1531 session.feed_data(&file_id, 1024, &file_data[1024..2048]);
1532
1533 assert!(session.is_complete());
1535 assert!(matches!(
1536 session.file_status(&file_id),
1537 Some(FileStatus::Complete)
1538 ));
1539 assert!(matches!(session.repairability(), Repairability::NotNeeded));
1540
1541 let result = session.verification_result().unwrap();
1543 assert_eq!(result.total_missing_blocks, 0);
1544 assert!(result.files[0].valid_slices.iter().all(|&v| v));
1545 }
1546
1547 #[test]
1548 fn session_detects_corrupted_slice() {
1549 let slice_size = 1024u64;
1550 let file_data: Vec<u8> = (0..2048u32).map(|i| (i % 256) as u8).collect();
1551 let (par2_bytes, file_id, _rsid) = build_par2_packets(&file_data, slice_size);
1552
1553 let mut session = VerificationSession::new();
1554 let packets = parse_packets(&par2_bytes);
1555 session.add_par2_data(&packets);
1556
1557 session.feed_data(&file_id, 0, &file_data[0..1024]);
1559
1560 let mut corrupted = file_data[1024..2048].to_vec();
1562 corrupted[0] ^= 0xFF;
1563 session.feed_data(&file_id, 1024, &corrupted);
1564
1565 assert!(!session.is_complete());
1567
1568 assert!(matches!(
1570 session.file_status(&file_id),
1571 Some(FileStatus::Damaged(1))
1572 ));
1573
1574 let result = session.verification_result().unwrap();
1575 assert_eq!(result.total_missing_blocks, 1);
1576 assert!(result.files[0].valid_slices[0]); assert!(!result.files[0].valid_slices[1]); }
1579
1580 #[test]
1581 fn session_handles_data_before_metadata() {
1582 let slice_size = 1024u64;
1583 let file_data: Vec<u8> = (0..2048u32).map(|i| (i % 256) as u8).collect();
1584 let (par2_bytes, file_id, _rsid) = build_par2_packets(&file_data, slice_size);
1585
1586 let mut session = VerificationSession::new();
1587
1588 session.feed_data(&file_id, 0, &file_data[0..1024]);
1590 session.feed_data(&file_id, 1024, &file_data[1024..2048]);
1591
1592 assert!(session.par2_set().is_none());
1594 assert!(!session.is_complete());
1595 assert!(session.verification_result().is_none());
1596
1597 let packets = parse_packets(&par2_bytes);
1599 session.add_par2_data(&packets);
1600
1601 assert!(!session.is_complete());
1603
1604 session.feed_data(&file_id, 0, &file_data[0..1024]);
1606 session.feed_data(&file_id, 1024, &file_data[1024..2048]);
1607
1608 assert!(session.is_complete());
1609 }
1610
1611 #[test]
1612 fn session_repairability_mid_stream() {
1613 let slice_size = 1024u64;
1614 let file_data: Vec<u8> = (0..4096u32).map(|i| (i % 256) as u8).collect();
1615 let (par2_bytes, file_id, _rsid) = build_par2_packets(&file_data, slice_size);
1616
1617 let mut session = VerificationSession::new();
1618 let packets = parse_packets(&par2_bytes);
1619 session.add_par2_data(&packets);
1620
1621 match session.repairability() {
1623 Repairability::Insufficient {
1624 blocks_needed: 4, ..
1625 } => {}
1626 other => panic!("expected Insufficient with 4 blocks needed, got {other:?}"),
1627 }
1628
1629 session.feed_data(&file_id, 0, &file_data[0..1024]);
1631
1632 let mut corrupted = file_data[1024..2048].to_vec();
1639 corrupted[0] ^= 0xFF;
1640 session.feed_data(&file_id, 1024, &corrupted);
1641
1642 match session.repairability() {
1644 Repairability::Insufficient {
1645 blocks_needed: 1,
1646 blocks_available: 0,
1647 ..
1648 } => {}
1649 other => panic!("expected Insufficient with 1 block needed, got {other:?}"),
1650 }
1651 }
1652
1653 #[test]
1654 fn session_integration_with_repair_plan() {
1655 use crate::gf;
1656 use crate::repair::plan_repair;
1657
1658 let slice_size = 64u64;
1659 let file_data: Vec<u8> = (0..256u32).map(|i| (i % 256) as u8).collect();
1660 let (par2_bytes, file_id, _rsid) = build_par2_packets(&file_data, slice_size);
1661
1662 let mut session = VerificationSession::new();
1663 let packets = parse_packets(&par2_bytes);
1664 session.add_par2_data(&packets);
1665
1666 {
1668 let set = Arc::make_mut(session.par2_set.as_mut().unwrap());
1669 let num_slices = 4; let constants = gf::input_slice_constants(num_slices);
1671 let ss = slice_size as usize;
1672 let word_count = ss / 2;
1673
1674 let mut padded = file_data.clone();
1675 padded.resize(num_slices * ss, 0);
1676
1677 for r in 0..2u32 {
1678 let mut recovery = vec![0u8; ss];
1679 for (i, &constant) in constants.iter().enumerate() {
1680 let factor = gf::pow(constant, r);
1681 for w in 0..word_count {
1682 let input_word = u16::from_le_bytes([
1683 padded[i * ss + w * 2],
1684 padded[i * ss + w * 2 + 1],
1685 ]);
1686 let contribution = gf::mul(input_word, factor);
1687 let rec_word = u16::from_le_bytes([recovery[w * 2], recovery[w * 2 + 1]]);
1688 let new_val = gf::add(rec_word, contribution);
1689 let bytes = new_val.to_le_bytes();
1690 recovery[w * 2] = bytes[0];
1691 recovery[w * 2 + 1] = bytes[1];
1692 }
1693 }
1694 set.recovery_slices.insert(
1695 r,
1696 RecoverySlice {
1697 exponent: r,
1698 data: Bytes::from(recovery).into(),
1699 },
1700 );
1701 }
1702 }
1703
1704 session.feed_data(&file_id, 0, &file_data[0..64]);
1706 session.feed_data(&file_id, 64, &file_data[64..128]);
1707
1708 let mut corrupted = file_data[128..192].to_vec();
1709 corrupted[0] ^= 0xFF;
1710 session.feed_data(&file_id, 128, &corrupted);
1711
1712 session.feed_data(&file_id, 192, &file_data[192..256]);
1713
1714 let result = session.verification_result().unwrap();
1716 assert_eq!(result.total_missing_blocks, 1);
1717
1718 let plan = plan_repair(session.par2_set().unwrap(), &result).unwrap();
1719 assert_eq!(plan.missing_slices.len(), 1);
1720 assert_eq!(plan.missing_slices[0], (file_id, 2));
1721 }
1722
1723 #[test]
1724 fn session_partial_last_slice() {
1725 let slice_size = 1024u64;
1726 let file_data: Vec<u8> = (0..1500u32).map(|i| (i % 256) as u8).collect();
1728 let (par2_bytes, file_id, _rsid) = build_par2_packets(&file_data, slice_size);
1729
1730 let mut session = VerificationSession::new();
1731 let packets = parse_packets(&par2_bytes);
1732 session.add_par2_data(&packets);
1733
1734 session.feed_data(&file_id, 0, &file_data[0..1024]);
1735 session.feed_data(&file_id, 1024, &file_data[1024..1500]);
1736
1737 assert!(session.is_complete());
1738 let result = session.verification_result().unwrap();
1739 assert_eq!(result.total_missing_blocks, 0);
1740 }
1741
1742 #[test]
1743 fn session_verify_from_slice_crcs() {
1744 let slice_size = 1024u64;
1745 let file_data: Vec<u8> = (0..2048u32).map(|i| (i % 256) as u8).collect();
1746 let (par2_bytes, file_id, _rsid) = build_par2_packets(&file_data, slice_size);
1747
1748 let mut session = VerificationSession::new();
1749 let packets = parse_packets(&par2_bytes);
1750 session.add_par2_data(&packets);
1751
1752 let crc0 = checksum::crc32(&file_data[0..1024]);
1754 let crc1 = checksum::crc32(&file_data[1024..2048]);
1755
1756 let result = session
1757 .verify_from_slice_crcs(&file_id, &[crc0, crc1])
1758 .unwrap();
1759 assert_eq!(result, vec![true, true]);
1760
1761 assert!(session.is_complete());
1763 assert!(matches!(
1764 session.file_status(&file_id),
1765 Some(FileStatus::Complete)
1766 ));
1767 assert!(
1768 session
1769 .slice_evidence()
1770 .iter()
1771 .all(|evidence| evidence.strength() == SliceEvidenceStrength::Crc32Only)
1772 );
1773 }
1774
1775 #[test]
1776 fn session_verify_from_slice_crcs_partial_damage() {
1777 let slice_size = 1024u64;
1778 let file_data: Vec<u8> = (0..2048u32).map(|i| (i % 256) as u8).collect();
1779 let (par2_bytes, file_id, _rsid) = build_par2_packets(&file_data, slice_size);
1780
1781 let mut session = VerificationSession::new();
1782 let packets = parse_packets(&par2_bytes);
1783 session.add_par2_data(&packets);
1784
1785 let crc0 = checksum::crc32(&file_data[0..1024]);
1786 let wrong_crc = 0xDEADBEEF;
1787
1788 let result = session
1789 .verify_from_slice_crcs(&file_id, &[crc0, wrong_crc])
1790 .unwrap();
1791 assert_eq!(result, vec![true, false]);
1792
1793 assert!(!session.is_complete());
1794 assert!(matches!(
1795 session.file_status(&file_id),
1796 Some(FileStatus::Damaged(1))
1797 ));
1798 }
1799
1800 #[test]
1801 fn session_empty_before_metadata() {
1802 let session = VerificationSession::new();
1803 assert!(!session.is_complete());
1804 assert!(session.verification_result().is_none());
1805 assert!(matches!(session.repairability(), Repairability::NotNeeded));
1806 }
1807
1808 #[test]
1809 fn feed_range_settles_out_of_order_boundaries_without_buffering_full_slices() {
1810 let slice_size = 8u64;
1811 let file_data: Vec<u8> = (0..24u8).collect();
1812 let (par2_bytes, file_id, _) = build_par2_packets(&file_data, slice_size);
1813 let mut session = VerificationSession::new();
1814 session.add_par2_data(&parse_packets(&par2_bytes));
1815
1816 let pending = session.feed_range(&file_id, 11, &file_data[11..16]);
1819 assert_eq!(pending.disposition(), FeedDisposition::Buffered);
1820 assert_eq!(pending.settle_reads().len(), 1);
1821 assert_eq!(pending.settle_reads()[0].offset(), 8);
1822 assert_eq!(pending.settle_reads()[0].length(), 3);
1823
1824 let settled = session.feed_range(&file_id, 8, &file_data[8..11]);
1825 assert_eq!(settled.disposition(), FeedDisposition::Verified);
1826 assert_eq!(settled.evidence().len(), 1);
1827 assert_eq!(settled.evidence()[0].slice_index(), 1);
1828 assert!(settled.evidence()[0].is_valid());
1829
1830 assert_eq!(
1832 session
1833 .feed_range(&file_id, 3, &file_data[3..8])
1834 .disposition(),
1835 FeedDisposition::Buffered
1836 );
1837 let settled = session.feed_range(&file_id, 0, &file_data[..3]);
1838 assert_eq!(settled.disposition(), FeedDisposition::Verified);
1839 assert_eq!(settled.evidence()[0].slice_index(), 0);
1840
1841 let direct = session.feed_range(&file_id, 16, &file_data[16..24]);
1844 assert_eq!(direct.disposition(), FeedDisposition::Verified);
1845 assert_eq!(direct.evidence()[0].slice_index(), 2);
1846 assert!(session.is_complete());
1847 }
1848
1849 #[test]
1850 fn feed_range_distinguishes_identical_and_conflicting_overlaps() {
1851 let slice_size = 8u64;
1852 let file_data: Vec<u8> = (0..8u8).collect();
1853 let (par2_bytes, file_id, _) = build_par2_packets(&file_data, slice_size);
1854 let reserved = 4 + PARTIAL_RANGE_ACCOUNTING_BYTES;
1855 let budget = VerificationMemoryBudget::new(reserved);
1856 let mut session = VerificationSession::with_memory_budget(budget.clone());
1857 session.add_par2_data(&parse_packets(&par2_bytes));
1858
1859 assert_eq!(
1860 session
1861 .feed_range(&file_id, 0, &file_data[..4])
1862 .disposition(),
1863 FeedDisposition::Buffered
1864 );
1865 assert_eq!(budget.buffered_bytes(), reserved);
1866
1867 let duplicate = session.feed_range(&file_id, 2, &file_data[2..4]);
1870 assert_eq!(duplicate.disposition(), FeedDisposition::Duplicate);
1871 assert_eq!(budget.buffered_bytes(), reserved);
1872
1873 let mut conflicting = file_data[3..5].to_vec();
1874 conflicting[0] ^= 0xFF;
1875 let conflict = session.feed_range(&file_id, 3, &conflicting);
1876 assert_eq!(conflict.disposition(), FeedDisposition::ConflictingOverlap);
1877 assert_eq!(budget.buffered_bytes(), 0);
1878 assert_eq!(conflict.settle_reads().len(), 1);
1879 assert_eq!(conflict.settle_reads()[0].offset(), 0);
1880 assert_eq!(conflict.settle_reads()[0].length(), slice_size);
1881
1882 let settled = session.feed_range(&file_id, 0, &file_data);
1883 assert_eq!(settled.disposition(), FeedDisposition::Verified);
1884 assert!(settled.evidence()[0].is_valid());
1885 }
1886
1887 #[test]
1888 fn feed_range_reports_metadata_and_out_of_range_input() {
1889 let slice_size = 8u64;
1890 let file_data: Vec<u8> = (0..8u8).collect();
1891 let (par2_bytes, file_id, _) = build_par2_packets(&file_data, slice_size);
1892 let mut session = VerificationSession::new();
1893
1894 assert_eq!(
1895 session.feed_range(&file_id, 0, &file_data).disposition(),
1896 FeedDisposition::MetadataPending
1897 );
1898
1899 session.add_par2_data(&parse_packets(&par2_bytes));
1900 assert_eq!(
1901 session
1902 .feed_range(&file_id, file_data.len() as u64, &[1])
1903 .disposition(),
1904 FeedDisposition::OutOfRange
1905 );
1906 assert_eq!(
1907 session.feed_range(&file_id, u64::MAX, &[1]).disposition(),
1908 FeedDisposition::OutOfRange
1909 );
1910 }
1911
1912 #[test]
1913 fn feed_range_pads_a_partial_last_slice_after_arbitrary_ranges() {
1914 let slice_size = 8u64;
1915 let file_data: Vec<u8> = (0..13u8).collect();
1916 let (par2_bytes, file_id, _) = build_par2_packets(&file_data, slice_size);
1917 let mut session = VerificationSession::new();
1918 session.add_par2_data(&parse_packets(&par2_bytes));
1919
1920 assert_eq!(
1921 session
1922 .feed_range(&file_id, 0, &file_data[..8])
1923 .disposition(),
1924 FeedDisposition::Verified
1925 );
1926 assert_eq!(
1927 session
1928 .feed_range(&file_id, 10, &file_data[10..])
1929 .disposition(),
1930 FeedDisposition::Buffered
1931 );
1932 let last = session.feed_range(&file_id, 8, &file_data[8..10]);
1933 assert_eq!(last.disposition(), FeedDisposition::Verified);
1934 assert_eq!(last.evidence()[0].slice_index(), 1);
1935 assert!(last.evidence()[0].is_valid());
1936 assert!(session.is_complete());
1937 }
1938
1939 #[test]
1940 fn shared_boundary_budget_is_released_on_settlement_and_drop() {
1941 let slice_size = 8u64;
1942 let file_data: Vec<u8> = (0..8u8).collect();
1943 let (par2_bytes, file_id, _) = build_par2_packets(&file_data, slice_size);
1944 let packets = parse_packets(&par2_bytes);
1945 let reserved = 4 + PARTIAL_RANGE_ACCOUNTING_BYTES;
1946 let budget = VerificationMemoryBudget::new(reserved);
1947 let options = VerificationSessionOptions::new().with_memory_budget(budget.clone());
1948 let mut first = VerificationSession::with_options(options.clone());
1949 let mut second = VerificationSession::with_options(options);
1950 first.add_par2_data(&packets);
1951 second.add_par2_data(&packets);
1952
1953 assert_eq!(
1954 first.feed_range(&file_id, 0, &file_data[..4]).disposition(),
1955 FeedDisposition::Buffered
1956 );
1957 assert_eq!(budget.buffered_bytes(), reserved);
1958 assert_eq!(
1959 second
1960 .feed_range(&file_id, 0, &file_data[..4])
1961 .disposition(),
1962 FeedDisposition::BudgetExhausted
1963 );
1964
1965 assert_eq!(
1968 first.feed_range(&file_id, 0, &file_data).disposition(),
1969 FeedDisposition::Verified
1970 );
1971 assert_eq!(budget.buffered_bytes(), 0);
1972 assert_eq!(
1973 second
1974 .feed_range(&file_id, 0, &file_data[..4])
1975 .disposition(),
1976 FeedDisposition::Buffered
1977 );
1978 assert_eq!(budget.buffered_bytes(), reserved);
1979
1980 drop(second);
1981 assert_eq!(budget.buffered_bytes(), 0);
1982 }
1983
1984 #[test]
1985 fn fragmented_range_metadata_counts_toward_shared_budget() {
1986 let slice_size = 8u64;
1987 let file_data: Vec<u8> = (0..8u8).collect();
1988 let (par2_bytes, file_id, _) = build_par2_packets(&file_data, slice_size);
1989 let budget = VerificationMemoryBudget::new(2 + PARTIAL_RANGE_ACCOUNTING_BYTES * 2 - 1);
1990 let mut session = VerificationSession::with_memory_budget(budget.clone());
1991 session.add_par2_data(&parse_packets(&par2_bytes));
1992
1993 assert_eq!(
1994 session
1995 .feed_range(&file_id, 0, &file_data[..1])
1996 .disposition(),
1997 FeedDisposition::Buffered
1998 );
1999 assert_eq!(
2000 session
2001 .feed_range(&file_id, 2, &file_data[2..3])
2002 .disposition(),
2003 FeedDisposition::BudgetExhausted
2004 );
2005 assert_eq!(budget.buffered_bytes(), 1 + PARTIAL_RANGE_ACCOUNTING_BYTES);
2006 }
2007
2008 #[test]
2009 fn add_par2_data_merges_new_packets_without_rebuilding_an_existing_set() {
2010 let slice_size = 8u64;
2011 let file_data: Vec<u8> = (0..8u8).collect();
2012 let (par2_bytes, _file_id, _) = build_par2_packets(&file_data, slice_size);
2013 let packets = parse_packets(&par2_bytes);
2014 let mut session = VerificationSession::new();
2015 session.add_par2_data(&packets);
2016
2017 let before = Arc::as_ptr(session.par2_set.as_ref().unwrap());
2018 session.add_par2_data(&packets[..1]);
2021 assert_eq!(before, Arc::as_ptr(session.par2_set.as_ref().unwrap()));
2022 }
2023
2024 #[test]
2025 fn in_stream_proof_refuses_every_incomplete_attestation() {
2026 let proof = InStreamCrc32Proof::try_new(4096, true, true, true)
2027 .expect("a complete attestation should prove");
2028 assert_eq!(proof.covered_length(), 4096);
2029
2030 assert!(matches!(
2031 InStreamCrc32Proof::try_new(0, true, true, true),
2032 Err(InStreamCrc32ProofError::EmptyCoverage)
2033 ));
2034 assert!(matches!(
2035 InStreamCrc32Proof::try_new(4096, false, true, true),
2036 Err(InStreamCrc32ProofError::IncompleteSliceCoverage)
2037 ));
2038 assert!(matches!(
2039 InStreamCrc32Proof::try_new(4096, true, false, true),
2040 Err(InStreamCrc32ProofError::UnverifiedSourceBytes)
2041 ));
2042 assert!(matches!(
2043 InStreamCrc32Proof::try_new(4096, true, true, false),
2044 Err(InStreamCrc32ProofError::NoIndependentCrc32Coverage)
2045 ));
2046 }
2047
2048 #[test]
2051 fn in_stream_evidence_reports_crc32_only_strength_and_is_admissible() {
2052 let set_id = RecoverySetId::from_bytes([0x51; 16]);
2053 let file_id = FileId::from_bytes([0x52; 16]);
2054 let proof = InStreamCrc32Proof::try_new(1024, true, true, true).unwrap();
2055 let evidence = SliceEvidence::from_in_stream_crc32(set_id, file_id, 7, true, proof);
2056
2057 assert_eq!(evidence.recovery_set_id(), set_id);
2058 assert_eq!(evidence.file_id(), file_id);
2059 assert_eq!(evidence.slice_index(), 7);
2060 assert!(evidence.is_valid());
2061 assert_eq!(evidence.strength(), SliceEvidenceStrength::Crc32Only);
2062 assert_eq!(evidence.in_stream_proof(), Some(&proof));
2063 assert!(evidence.may_seed_repair_input());
2064
2065 let damaged = SliceEvidence::from_in_stream_crc32(set_id, file_id, 7, false, proof);
2068 assert!(!damaged.is_valid());
2069 assert!(damaged.may_seed_repair_input());
2070 }
2071
2072 #[test]
2076 fn session_hashed_crc32_only_evidence_carries_no_attestation() {
2077 let slice_size = 8u64;
2078 let file_data: Vec<u8> = (0..16u8).collect();
2079 let (par2_bytes, file_id, _) = build_par2_packets(&file_data, slice_size);
2080 let mut session = VerificationSession::new();
2081 session.add_par2_data(&parse_packets(&par2_bytes));
2082
2083 let crcs: Vec<u32> = file_data
2084 .chunks(slice_size as usize)
2085 .map(|slice| {
2086 let mut state = SliceChecksumState::new();
2087 state.update(slice);
2088 state.finalize(Some(slice_size)).0
2089 })
2090 .collect();
2091 session
2092 .verify_from_slice_crcs(&file_id, &crcs)
2093 .expect("slice CRCs settle the file");
2094
2095 let evidence = session.slice_evidence();
2096 assert!(!evidence.is_empty());
2097 for entry in evidence {
2098 assert_eq!(entry.strength(), SliceEvidenceStrength::Crc32Only);
2099 assert_eq!(entry.in_stream_proof(), None);
2100 assert!(!entry.may_seed_repair_input());
2101 }
2102 }
2103}