1use std::collections::hash_map::Entry;
50use std::collections::{HashMap, HashSet};
51use std::fmt;
52use std::io::{Read, Seek, SeekFrom, Write};
53use std::path::Path;
54
55use rayon::prelude::*;
56use tracing::{debug, info};
57
58use crate::gf;
59use crate::gf_simd;
60use crate::matrix::{GfMatrix, par2_input_constants};
61use crate::recovery::{RecoveryBlock, load_recovery_blocks};
62use crate::types::{Id16, Par2FileSet, VerifyResult};
63use crate::verify;
64
65#[derive(Debug)]
67pub struct RepairResult {
68 pub success: bool,
70 pub blocks_repaired: u32,
72 pub files_repaired: usize,
74 pub message: String,
76}
77
78impl fmt::Display for RepairResult {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 if self.success {
81 write!(
82 f,
83 "Repair complete: {} blocks repaired across {} files",
84 self.blocks_repaired, self.files_repaired
85 )
86 } else {
87 write!(f, "Repair failed: {}", self.message)
88 }
89 }
90}
91
92#[derive(Debug, thiserror::Error)]
94pub enum RepairError {
95 #[error("I/O error: {0}")]
96 Io(#[from] std::io::Error),
97 #[error("Insufficient recovery data: need {needed} blocks, have {available}")]
98 InsufficientRecovery { needed: u32, available: u32 },
99 #[error("Decode matrix is singular — cannot repair with these recovery blocks")]
100 SingularMatrix,
101 #[error("No damage detected — nothing to repair")]
102 NoDamage,
103 #[error("Verification after repair failed: {0}")]
104 VerifyFailed(String),
105 #[error("Invalid slice size {0}: must be a non-zero multiple of 4")]
106 InvalidSliceSize(usize),
107 #[error("Recovery block {exponent} is {len} bytes, expected the slice size {slice_size}")]
108 InvalidRecoveryBlock {
109 exponent: u32,
110 len: usize,
111 slice_size: usize,
112 },
113 #[error("Slice index {index} out of range: the set has {total} input slices")]
114 SliceOutOfRange { index: usize, total: usize },
115 #[error("Slice {index} is {len} bytes, longer than the slice size {slice_size}")]
116 SliceTooLong {
117 index: usize,
118 len: usize,
119 slice_size: usize,
120 },
121 #[error("Slice {index} was already fed (feeding twice cancels it out)")]
122 DuplicateSlice { index: usize },
123 #[error("{unaccounted} input slices were neither fed nor listed as missing")]
124 IncompleteFeed { unaccounted: usize },
125}
126
127#[derive(Debug, Clone)]
147pub struct Repair {
148 slice_size: usize,
149 constants: Vec<u16>,
151 exponents: Vec<u32>,
153 rows: Vec<Vec<u8>>,
155 fed: Vec<bool>,
156 fed_count: usize,
157}
158
159impl Repair {
160 pub fn new(
162 recovery: &[RecoveryBlock],
163 slice_size: usize,
164 total_input_slices: usize,
165 ) -> Result<Self, RepairError> {
166 Self::new_owning(recovery.to_vec(), slice_size, total_input_slices)
167 }
168
169 pub fn new_owning(
175 mut recovery: Vec<RecoveryBlock>,
176 slice_size: usize,
177 total_input_slices: usize,
178 ) -> Result<Self, RepairError> {
179 if slice_size == 0 || slice_size % 4 != 0 {
180 return Err(RepairError::InvalidSliceSize(slice_size));
181 }
182 if let Some(bad) = recovery.iter().find(|b| b.data.len() != slice_size) {
183 return Err(RepairError::InvalidRecoveryBlock {
184 exponent: bad.exponent,
185 len: bad.data.len(),
186 slice_size,
187 });
188 }
189 recovery.sort_by_key(|b| b.exponent);
190 recovery.dedup_by_key(|b| b.exponent);
191
192 let (exponents, rows) = recovery.into_iter().map(|b| (b.exponent, b.data)).unzip();
193 Ok(Self {
194 slice_size,
195 constants: par2_input_constants(total_input_slices),
196 exponents,
197 rows,
198 fed: vec![false; total_input_slices],
199 fed_count: 0,
200 })
201 }
202
203 pub fn rows(&self) -> usize {
206 self.rows.len()
207 }
208
209 pub fn exponents(&self) -> &[u32] {
211 &self.exponents
212 }
213
214 pub fn slice_size(&self) -> usize {
216 self.slice_size
217 }
218
219 pub fn total_slices(&self) -> usize {
221 self.fed.len()
222 }
223
224 pub fn fed_count(&self) -> usize {
226 self.fed_count
227 }
228
229 pub fn is_fed(&self, index: usize) -> bool {
231 self.fed.get(index).copied().unwrap_or(false)
232 }
233
234 pub fn truncate_rows(&mut self, n: usize) {
240 self.rows.truncate(n);
241 self.exponents.truncate(n);
242 }
243
244 pub fn feed(&mut self, index: usize, data: &[u8]) -> Result<(), RepairError> {
249 self.check_feed(index, data)?;
250 let c = self.constants[index];
251 self.rows
252 .par_iter_mut()
253 .zip(self.exponents.par_iter())
254 .for_each(|(row, &exp)| mul_add_prefix(row, data, gf::pow(c, exp)));
255 self.fed[index] = true;
256 self.fed_count += 1;
257 Ok(())
258 }
259
260 pub fn feed_batch(&mut self, slices: &[(usize, &[u8])]) -> Result<(), RepairError> {
267 for (k, &(index, data)) in slices.iter().enumerate() {
268 if let Err(e) = self.check_feed(index, data) {
269 for &(earlier, _) in &slices[..k] {
270 self.fed[earlier] = false;
271 }
272 return Err(e);
273 }
274 self.fed[index] = true;
276 }
277
278 let slice_size = self.slice_size;
279 let (full, short): (Vec<_>, Vec<_>) = slices
280 .iter()
281 .copied()
282 .partition(|(_, data)| data.len() == slice_size);
283 let full_data: Vec<&[u8]> = full.iter().map(|&(_, d)| d).collect();
284
285 let constants = &self.constants;
286 self.rows
287 .par_iter_mut()
288 .zip(self.exponents.par_iter())
289 .for_each(|(row, &exp)| {
290 let coeffs: Vec<u16> = full
291 .iter()
292 .map(|&(i, _)| gf::pow(constants[i], exp))
293 .collect();
294 gf_simd::mul_add_multi(row, &full_data, &coeffs);
295 for &(i, data) in &short {
296 mul_add_prefix(row, data, gf::pow(constants[i], exp));
297 }
298 });
299
300 self.fed_count += slices.len();
301 Ok(())
302 }
303
304 pub fn solve(&self, missing: &[usize]) -> Result<HashMap<usize, Vec<u8>>, RepairError> {
316 let mut missing = missing.to_vec();
317 missing.sort_unstable();
318 missing.dedup();
319 let outputs = self.reconstruct(&missing)?;
320 Ok(missing.into_iter().zip(outputs).collect())
321 }
322
323 fn check_feed(&self, index: usize, data: &[u8]) -> Result<(), RepairError> {
324 let total = self.fed.len();
325 if index >= total {
326 return Err(RepairError::SliceOutOfRange { index, total });
327 }
328 if data.len() > self.slice_size {
329 return Err(RepairError::SliceTooLong {
330 index,
331 len: data.len(),
332 slice_size: self.slice_size,
333 });
334 }
335 if self.fed[index] {
336 return Err(RepairError::DuplicateSlice { index });
337 }
338 Ok(())
339 }
340
341 fn reconstruct(&self, missing: &[usize]) -> Result<Vec<Vec<u8>>, RepairError> {
343 let total = self.fed.len();
344 for &index in missing {
345 if index >= total {
346 return Err(RepairError::SliceOutOfRange { index, total });
347 }
348 if self.fed[index] {
349 return Err(RepairError::DuplicateSlice { index });
350 }
351 }
352 let unaccounted = total - self.fed_count - missing.len();
353 if unaccounted != 0 {
354 return Err(RepairError::IncompleteFeed { unaccounted });
355 }
356
357 let d = missing.len();
358 if d == 0 {
359 return Ok(Vec::new());
360 }
361 if d > self.rows.len() {
362 return Err(RepairError::InsufficientRecovery {
363 needed: d as u32,
364 available: self.rows.len() as u32,
365 });
366 }
367
368 let mut vandermonde = GfMatrix::zeros(d, d);
373 for (e, &exp) in self.exponents[..d].iter().enumerate() {
374 for (j, &idx) in missing.iter().enumerate() {
375 vandermonde.set(e, j, gf::pow(self.constants[idx], exp));
376 }
377 }
378 let inverse = vandermonde.invert().ok_or(RepairError::SingularMatrix)?;
379 debug!(d, "D×D decode matrix inverted");
380
381 let row_refs: Vec<&[u8]> = self.rows[..d].iter().map(Vec::as_slice).collect();
383 let mut outputs: Vec<Vec<u8>> = (0..d).map(|_| vec![0u8; self.slice_size]).collect();
384 outputs.par_iter_mut().enumerate().for_each(|(j, dst)| {
385 let coeffs: Vec<u16> = (0..d).map(|e| inverse.get(j, e)).collect();
386 gf_simd::mul_add_multi(dst, &row_refs, &coeffs);
387 });
388 Ok(outputs)
389 }
390}
391
392fn mul_add_prefix(dst: &mut [u8], src: &[u8], c: u16) {
396 let even = src.len() & !1;
397 gf_simd::mul_add_buffer(&mut dst[..even], &src[..even], c);
398 if even < src.len() {
399 let p = gf::mul(c, src[even] as u16);
400 dst[even] ^= p as u8;
401 dst[even + 1] ^= (p >> 8) as u8;
402 }
403}
404
405pub fn repair(file_set: &Par2FileSet, dir: &Path) -> Result<RepairResult, RepairError> {
417 let verify_result = verify::verify(file_set, dir);
418 repair_from_verify_inner(file_set, dir, &verify_result, true)
419}
420
421pub fn repair_from_verify(
438 file_set: &Par2FileSet,
439 dir: &Path,
440 verify_result: &VerifyResult,
441) -> Result<RepairResult, RepairError> {
442 repair_from_verify_inner(file_set, dir, verify_result, true)
443}
444
445pub fn repair_from_verify_no_reverify(
451 file_set: &Par2FileSet,
452 dir: &Path,
453 verify_result: &VerifyResult,
454) -> Result<RepairResult, RepairError> {
455 repair_from_verify_inner(file_set, dir, verify_result, false)
456}
457
458fn repair_from_verify_inner(
459 file_set: &Par2FileSet,
460 dir: &Path,
461 verify_result: &VerifyResult,
462 re_verify: bool,
463) -> Result<RepairResult, RepairError> {
464 if verify_result.all_correct() {
465 return Err(RepairError::NoDamage);
466 }
467
468 let blocks_needed = verify_result.blocks_needed();
469 info!(
470 blocks_needed,
471 damaged = verify_result.damaged.len(),
472 missing = verify_result.missing.len(),
473 "Repair: damage detected"
474 );
475
476 let recovery_blocks = load_recovery_blocks(dir, &file_set.recovery_set_id, file_set.slice_size);
478
479 let block_map = build_block_map(file_set);
481 let total_input_blocks = block_map.total_blocks as usize;
482 let slice_size = file_set.slice_size as usize;
483
484 let mut fold = Repair::new_owning(recovery_blocks, slice_size, total_input_blocks)?;
485 if (fold.rows() as u32) < blocks_needed {
486 return Err(RepairError::InsufficientRecovery {
487 needed: blocks_needed,
488 available: fold.rows() as u32,
489 });
490 }
491
492 let damaged_indices = find_damaged_block_indices(verify_result, &block_map);
493 info!(
494 damaged_block_count = damaged_indices.len(),
495 total_input_blocks, "Mapped damaged blocks to global indices"
496 );
497
498 fold.truncate_rows(damaged_indices.len());
500
501 let damaged_set: HashSet<usize> = damaged_indices.iter().copied().collect();
509 let intact_indices: Vec<usize> = (0..total_input_blocks)
510 .filter(|i| !damaged_set.contains(i))
511 .collect();
512
513 const BATCH_SIZE: usize = 24;
516
517 let mut file_handles: HashMap<String, std::fs::File> = HashMap::new();
519
520 for batch in intact_indices.chunks(BATCH_SIZE) {
521 let batch_data: Vec<Vec<u8>> = batch
522 .iter()
523 .map(|&idx| read_source_block(dir, &block_map, idx, slice_size, &mut file_handles))
524 .collect::<std::io::Result<Vec<_>>>()?;
525 let pairs: Vec<(usize, &[u8])> = batch
526 .iter()
527 .copied()
528 .zip(batch_data.iter().map(Vec::as_slice))
529 .collect();
530 fold.feed_batch(&pairs)?;
531 }
532
533 info!("Intact-block contributions subtracted from recovery data");
534
535 let mut repaired = fold.solve(&damaged_indices)?;
537 drop(fold);
538
539 info!("Repaired blocks reconstructed via D×D inverse");
540
541 let mut files_touched = HashSet::new();
543
544 for global_idx in &damaged_indices {
545 let data = repaired
546 .remove(global_idx)
547 .expect("solve returns every missing index");
548 let loc = block_map
549 .global_to_file(*global_idx)
550 .expect("damaged index comes from the block map");
551 let filename = &loc.file.filename;
552
553 let file_path = dir.join(filename);
554 debug!(
555 filename,
556 global_block = global_idx,
557 offset = loc.offset,
558 len = loc.len,
559 "Writing repaired block"
560 );
561
562 let mut f = std::fs::OpenOptions::new()
563 .create(true)
564 .truncate(false)
565 .write(true)
566 .open(&file_path)?;
567
568 if f.metadata()?.len() < loc.file.file_size {
570 f.set_len(loc.file.file_size)?;
571 }
572
573 f.seek(SeekFrom::Start(loc.offset))?;
574 f.write_all(&data[..loc.len])?;
575 files_touched.insert(filename.clone());
576 }
577
578 let blocks_repaired = damaged_indices.len() as u32;
579
580 if re_verify {
582 let verification = verify::verify(file_set, dir);
583 if verification.all_correct() {
584 info!(
585 blocks = blocks_repaired,
586 files = files_touched.len(),
587 "Repair successful — all files verified"
588 );
589 Ok(RepairResult {
590 success: true,
591 blocks_repaired,
592 files_repaired: files_touched.len(),
593 message: "All files repaired and verified".to_string(),
594 })
595 } else {
596 Err(RepairError::VerifyFailed(format!("{verification}")))
597 }
598 } else {
599 info!(
600 blocks = blocks_repaired,
601 files = files_touched.len(),
602 "Repair complete (re-verify skipped)"
603 );
604 Ok(RepairResult {
605 success: true,
606 blocks_repaired,
607 files_repaired: files_touched.len(),
608 message: "All files repaired (re-verify skipped)".to_string(),
609 })
610 }
611}
612
613#[derive(Debug, Clone)]
623pub struct BlockMap {
624 pub files: Vec<BlockFile>,
626 pub total_blocks: u32,
628 pub slice_size: u64,
630}
631
632#[derive(Debug, Clone)]
634pub struct BlockFile {
635 pub file_id: Id16,
637 pub filename: String,
639 pub file_size: u64,
641 pub block_count: u32,
643 pub start_block: u32,
645}
646
647#[derive(Debug, Clone, Copy)]
649pub struct SliceLocation<'a> {
650 pub file: &'a BlockFile,
652 pub local_index: u32,
654 pub offset: u64,
656 pub len: usize,
658}
659
660pub fn build_block_map(file_set: &Par2FileSet) -> BlockMap {
662 let slice_size = file_set.slice_size;
663 let mut files = Vec::new();
664 let mut block_offset = 0u32;
665
666 let ordered_files: Vec<_> = if file_set.file_order.is_empty() {
667 let mut fallback: Vec<_> = file_set.files.values().collect();
668 fallback.sort_by_key(|f| f.file_id);
669 fallback
670 } else {
671 file_set
672 .file_order
673 .iter()
674 .filter_map(|id| file_set.files.get(id))
675 .collect()
676 };
677
678 for f in ordered_files {
679 let block_count = if slice_size == 0 {
680 0
681 } else {
682 f.size.div_ceil(slice_size) as u32
683 };
684 files.push(BlockFile {
685 file_id: f.file_id,
686 filename: f.filename.clone(),
687 file_size: f.size,
688 block_count,
689 start_block: block_offset,
690 });
691 block_offset += block_count;
692 }
693
694 BlockMap {
695 files,
696 total_blocks: block_offset,
697 slice_size,
698 }
699}
700
701impl BlockMap {
702 pub fn new(file_set: &Par2FileSet) -> Self {
704 build_block_map(file_set)
705 }
706
707 pub fn global_to_file(&self, global_idx: usize) -> Option<SliceLocation<'_>> {
710 let global = u32::try_from(global_idx).ok()?;
711 let pos = self
714 .files
715 .partition_point(|f| f.start_block + f.block_count <= global);
716 let file = self.files.get(pos)?;
717 if global < file.start_block {
718 return None;
719 }
720 let local_index = global - file.start_block;
721 let offset = local_index as u64 * self.slice_size;
722 let len = (file.file_size - offset).min(self.slice_size) as usize;
723 Some(SliceLocation {
724 file,
725 local_index,
726 offset,
727 len,
728 })
729 }
730
731 pub fn global_index(&self, filename: &str, local_index: u32) -> Option<usize> {
733 let f = self.file(filename)?;
734 (local_index < f.block_count).then(|| (f.start_block + local_index) as usize)
735 }
736
737 pub fn file(&self, filename: &str) -> Option<&BlockFile> {
739 self.files.iter().find(|f| f.filename == filename)
740 }
741}
742
743fn find_damaged_block_indices(verify_result: &VerifyResult, block_map: &BlockMap) -> Vec<usize> {
744 let mut indices = Vec::new();
745
746 for damaged in &verify_result.damaged {
747 if let Some(bf) = block_map.file(&damaged.filename) {
748 if damaged.damaged_block_indices.is_empty() {
749 for i in 0..bf.block_count {
751 indices.push((bf.start_block + i) as usize);
752 }
753 } else {
754 for &local_idx in &damaged.damaged_block_indices {
756 indices.push((bf.start_block + local_idx) as usize);
757 }
758 }
759 }
760 }
761
762 for missing in &verify_result.missing {
763 if let Some(bf) = block_map.file(&missing.filename) {
764 for i in 0..bf.block_count {
765 indices.push((bf.start_block + i) as usize);
766 }
767 }
768 }
769
770 indices.sort();
771 indices.dedup();
772 indices
773}
774
775fn read_source_block(
777 dir: &Path,
778 block_map: &BlockMap,
779 global_idx: usize,
780 slice_size: usize,
781 file_handles: &mut HashMap<String, std::fs::File>,
782) -> std::io::Result<Vec<u8>> {
783 let loc = block_map
784 .global_to_file(global_idx)
785 .expect("intact index comes from the block map");
786
787 let handle = match file_handles.entry(loc.file.filename.clone()) {
788 Entry::Occupied(e) => e.into_mut(),
789 Entry::Vacant(e) => {
790 let path = dir.join(&loc.file.filename);
791 e.insert(std::fs::File::open(&path)?)
792 }
793 };
794 handle.seek(SeekFrom::Start(loc.offset))?;
795
796 let mut buf = vec![0u8; slice_size]; let mut total = 0;
798 while total < slice_size {
799 match handle.read(&mut buf[total..]) {
800 Ok(0) => break,
801 Ok(n) => total += n,
802 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
803 Err(e) => return Err(e),
804 }
805 }
806 Ok(buf)
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use std::path::PathBuf;
813
814 use crc32fast::hash as crc32_hash;
815 use md5::{Digest, Md5};
816
817 use crate::packets::{HEADER_SIZE, MAGIC};
818
819 const TYPE_MAIN: &[u8; 16] = b"PAR 2.0\x00Main\x00\x00\x00\x00";
820 const TYPE_FILE_DESC: &[u8; 16] = b"PAR 2.0\x00FileDesc";
821 const TYPE_IFSC: &[u8; 16] = b"PAR 2.0\x00IFSC\x00\x00\x00\x00";
822 const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
823
824 fn build_packet(set_id: [u8; 16], packet_type: &[u8; 16], body: &[u8]) -> Vec<u8> {
825 let mut data = Vec::with_capacity(32 + body.len());
826 data.extend_from_slice(&set_id);
827 data.extend_from_slice(packet_type);
828 data.extend_from_slice(body);
829
830 let mut packet = Vec::with_capacity(32 + data.len());
831 packet.extend_from_slice(MAGIC);
832 packet.extend_from_slice(&((HEADER_SIZE + body.len()) as u64).to_le_bytes());
833 packet.extend_from_slice(&Md5::digest(&data));
834 packet.extend_from_slice(&data);
835 packet
836 }
837
838 fn file_md5(data: &[u8]) -> [u8; 16] {
839 Md5::digest(data).into()
840 }
841
842 fn hash_16k(data: &[u8]) -> [u8; 16] {
843 Md5::digest(&data[..data.len().min(16_384)]).into()
844 }
845
846 fn padded_block(data: &[u8], block_index: usize, slice_size: usize) -> Vec<u8> {
847 let start = block_index * slice_size;
848 let end = (start + slice_size).min(data.len());
849 let mut block = vec![0u8; slice_size];
850 if start < data.len() {
851 block[..end - start].copy_from_slice(&data[start..end]);
852 }
853 block
854 }
855
856 fn write_par2_fixture(
857 dir: &Path,
858 slice_size: usize,
859 files: &[(&str, [u8; 16], Vec<u8>)],
860 main_order: &[[u8; 16]],
861 recovery_exponents: &[u32],
862 ) -> PathBuf {
863 let set_id = [0x5Au8; 16];
864 let mut index_bytes = Vec::new();
865
866 let mut main_body = Vec::new();
867 main_body.extend_from_slice(&(slice_size as u64).to_le_bytes());
868 main_body.extend_from_slice(&(main_order.len() as u32).to_le_bytes());
869 for file_id in main_order {
870 main_body.extend_from_slice(file_id);
871 }
872 index_bytes.extend_from_slice(&build_packet(set_id, TYPE_MAIN, &main_body));
873
874 for (filename, file_id, data) in files {
875 std::fs::write(dir.join(filename), data).unwrap();
876
877 let mut desc_body = Vec::new();
878 desc_body.extend_from_slice(file_id);
879 desc_body.extend_from_slice(&file_md5(data));
880 desc_body.extend_from_slice(&hash_16k(data));
881 desc_body.extend_from_slice(&(data.len() as u64).to_le_bytes());
882 desc_body.extend_from_slice(filename.as_bytes());
883 desc_body.push(0);
884 while desc_body.len() % 4 != 0 {
885 desc_body.push(0);
886 }
887 index_bytes.extend_from_slice(&build_packet(set_id, TYPE_FILE_DESC, &desc_body));
888
889 let block_count = data.len().div_ceil(slice_size);
890 let mut ifsc_body = Vec::new();
891 ifsc_body.extend_from_slice(file_id);
892 for block_idx in 0..block_count {
893 let block = padded_block(data, block_idx, slice_size);
894 ifsc_body.extend_from_slice(&file_md5(&block));
895 ifsc_body.extend_from_slice(&crc32_hash(&block).to_le_bytes());
896 }
897 index_bytes.extend_from_slice(&build_packet(set_id, TYPE_IFSC, &ifsc_body));
898 }
899
900 std::fs::write(dir.join("fixture.par2"), &index_bytes).unwrap();
901
902 let ordered_files: Vec<_> = main_order
903 .iter()
904 .map(|wanted| {
905 files
906 .iter()
907 .find(|(_, file_id, _)| file_id == wanted)
908 .unwrap()
909 })
910 .collect();
911 let input_blocks: Vec<Vec<u8>> = ordered_files
912 .iter()
913 .flat_map(|(_, _, data)| {
914 let count = data.len().div_ceil(slice_size);
915 (0..count).map(move |block_idx| padded_block(data, block_idx, slice_size))
916 })
917 .collect();
918
919 let input_count = input_blocks.len();
920 let enc = GfMatrix::par2_encoding_matrix(input_count, recovery_exponents);
921 let srcs: Vec<&[u8]> = input_blocks.iter().map(Vec::as_slice).collect();
922 let mut vol_bytes = index_bytes.clone();
923
924 for (row_idx, &exp) in recovery_exponents.iter().enumerate() {
925 let coeffs: Vec<u16> = (0..input_count)
926 .map(|col| enc.get(input_count + row_idx, col))
927 .collect();
928 let mut recovery = vec![0u8; slice_size];
929 gf_simd::mul_add_multi(&mut recovery, &srcs, &coeffs);
930
931 let mut body = Vec::with_capacity(4 + slice_size);
932 body.extend_from_slice(&exp.to_le_bytes());
933 body.extend_from_slice(&recovery);
934 vol_bytes.extend_from_slice(&build_packet(set_id, TYPE_RECOVERY, &body));
935 }
936
937 std::fs::write(dir.join("fixture.vol00+2.par2"), &vol_bytes).unwrap();
938 dir.join("fixture.par2")
939 }
940
941 #[test]
944 fn test_rs_roundtrip_simple() {
945 let input0: Vec<u8> = vec![0x01, 0x00, 0x02, 0x00]; let input1: Vec<u8> = vec![0x03, 0x00, 0x04, 0x00]; let input_count = 2;
950 let recovery_exponents = vec![0u32, 1u32];
951
952 let enc = GfMatrix::par2_encoding_matrix(input_count, &recovery_exponents);
954
955 let slice_size = 4;
957 let u16_per_slice = slice_size / 2;
958 let inputs = [&input0, &input1];
959
960 let mut recovery0 = vec![0u8; slice_size];
961 let mut recovery1 = vec![0u8; slice_size];
962
963 for pos in 0..u16_per_slice {
964 let off = pos * 2;
965 let mut r0: u16 = 0;
966 let mut r1: u16 = 0;
967 for (i, inp) in inputs.iter().enumerate() {
968 let val = u16::from_le_bytes([inp[off], inp[off + 1]]);
969 r0 = gf::add(r0, gf::mul(enc.get(2, i), val));
970 r1 = gf::add(r1, gf::mul(enc.get(3, i), val));
971 }
972 recovery0[off] = r0 as u8;
973 recovery0[off + 1] = (r0 >> 8) as u8;
974 recovery1[off] = r1 as u8;
975 recovery1[off + 1] = (r1 >> 8) as u8;
976 }
977
978 let constants = par2_input_constants(input_count);
984 let damaged_indices = [0usize, 1usize];
985
986 let num_damaged = damaged_indices.len();
988 let mut vandermonde = GfMatrix::zeros(num_damaged, num_damaged);
989 for (e, &exp) in recovery_exponents.iter().enumerate() {
990 for (j, &dmg_idx) in damaged_indices.iter().enumerate() {
991 vandermonde.set(e, j, gf::pow(constants[dmg_idx], exp));
992 }
993 }
994
995 let inv = vandermonde.invert().expect("Should be invertible");
996
997 let adjusted = [&recovery0[..], &recovery1[..]];
999 let mut result0 = vec![0u8; slice_size];
1000 let mut result1 = vec![0u8; slice_size];
1001
1002 for pos in 0..u16_per_slice {
1003 let off = pos * 2;
1004 let mut out0: u16 = 0;
1005 let mut out1: u16 = 0;
1006 for (e, adj) in adjusted.iter().enumerate() {
1007 let val = u16::from_le_bytes([adj[off], adj[off + 1]]);
1008 out0 = gf::add(out0, gf::mul(inv.get(0, e), val));
1009 out1 = gf::add(out1, gf::mul(inv.get(1, e), val));
1010 }
1011 result0[off] = out0 as u8;
1012 result0[off + 1] = (out0 >> 8) as u8;
1013 result1[off] = out1 as u8;
1014 result1[off + 1] = (out1 >> 8) as u8;
1015 }
1016
1017 assert_eq!(result0, input0, "Recovered block 0 should match original");
1018 assert_eq!(result1, input1, "Recovered block 1 should match original");
1019 }
1020
1021 #[test]
1024 fn test_rs_roundtrip_partial_damage() {
1025 let slice_size = 4;
1026 let input_count = 4;
1027 let recovery_exponents = vec![0u32, 1u32];
1028
1029 let inputs: Vec<Vec<u8>> = vec![
1030 vec![0x01, 0x00, 0x02, 0x00],
1031 vec![0x03, 0x00, 0x04, 0x00],
1032 vec![0x05, 0x00, 0x06, 0x00],
1033 vec![0x07, 0x00, 0x08, 0x00],
1034 ];
1035
1036 let enc = GfMatrix::par2_encoding_matrix(input_count, &recovery_exponents);
1037
1038 let mut recovery = vec![vec![0u8; slice_size]; 2];
1040 for pos in 0..(slice_size / 2) {
1041 let off = pos * 2;
1042 for (e, rec) in recovery.iter_mut().enumerate() {
1043 let mut val: u16 = 0;
1044 for (i, inp) in inputs.iter().enumerate() {
1045 let d = u16::from_le_bytes([inp[off], inp[off + 1]]);
1046 val = gf::add(val, gf::mul(enc.get(input_count + e, i), d));
1047 }
1048 rec[off] = val as u8;
1049 rec[off + 1] = (val >> 8) as u8;
1050 }
1051 }
1052
1053 let damaged_indices = [1usize, 3usize];
1055 let intact_indices: Vec<usize> = (0..input_count)
1056 .filter(|i| !damaged_indices.contains(i))
1057 .collect();
1058 let num_damaged = damaged_indices.len();
1059
1060 let constants = par2_input_constants(input_count);
1061
1062 let mut vandermonde = GfMatrix::zeros(num_damaged, num_damaged);
1064 for (e, &exp) in recovery_exponents.iter().enumerate() {
1065 for (j, &dmg_idx) in damaged_indices.iter().enumerate() {
1066 vandermonde.set(e, j, gf::pow(constants[dmg_idx], exp));
1067 }
1068 }
1069 let inv = vandermonde.invert().expect("Should be invertible");
1070
1071 let mut adjusted = recovery.clone();
1073 for &intact_idx in &intact_indices {
1074 let c_i = constants[intact_idx];
1075 for (e, adj) in adjusted.iter_mut().enumerate() {
1076 let coeff = gf::pow(c_i, recovery_exponents[e]);
1077 gf_simd::mul_add_buffer(adj, &inputs[intact_idx], coeff);
1078 }
1079 }
1080
1081 let adj_refs: Vec<&[u8]> = adjusted.iter().map(|v| v.as_slice()).collect();
1083 let mut outputs: Vec<Vec<u8>> = (0..num_damaged).map(|_| vec![0u8; slice_size]).collect();
1084
1085 for (j, dst) in outputs.iter_mut().enumerate() {
1086 let coeffs: Vec<u16> = (0..num_damaged).map(|e| inv.get(j, e)).collect();
1087 gf_simd::mul_add_multi(dst, &adj_refs, &coeffs);
1088 }
1089
1090 assert_eq!(outputs[0], inputs[1], "Recovered block 1 should match");
1092 assert_eq!(outputs[1], inputs[3], "Recovered block 3 should match");
1093 }
1094
1095 #[test]
1096 fn test_repair_cross_file_damage_respects_main_packet_file_order() {
1097 let dir = tempfile::tempdir().unwrap();
1098 let slice_size = 4096usize;
1099 let file_a_id = [0x20; 16];
1100 let file_b_id = [0x10; 16];
1101 let file_a = vec![0x41; slice_size * 2];
1102 let file_b = vec![0x42; slice_size * 2];
1103
1104 let par2_path = write_par2_fixture(
1105 dir.path(),
1106 slice_size,
1107 &[
1108 ("file_a.bin", file_a_id, file_a.clone()),
1109 ("file_b.bin", file_b_id, file_b.clone()),
1110 ],
1111 &[file_a_id, file_b_id],
1112 &[0, 1],
1113 );
1114
1115 let file_set = crate::parse(&par2_path).unwrap();
1116 assert_eq!(file_set.file_order, vec![file_a_id, file_b_id]);
1117
1118 {
1119 let file_a_path = dir.path().join("file_a.bin");
1120 let file_b_path = dir.path().join("file_b.bin");
1121 let mut a = std::fs::OpenOptions::new()
1122 .write(true)
1123 .open(&file_a_path)
1124 .unwrap();
1125 let mut b = std::fs::OpenOptions::new()
1126 .write(true)
1127 .open(&file_b_path)
1128 .unwrap();
1129 a.seek(SeekFrom::Start(slice_size as u64)).unwrap();
1130 b.seek(SeekFrom::Start(slice_size as u64)).unwrap();
1131 a.write_all(&vec![0xDE; slice_size]).unwrap();
1132 b.write_all(&vec![0xAD; slice_size]).unwrap();
1133 }
1134
1135 let pre = verify::verify(&file_set, dir.path());
1136 assert_eq!(pre.damaged.len(), 2);
1137 assert_eq!(pre.blocks_needed(), 2);
1138 assert!(pre.repair_possible);
1139
1140 let repaired = repair(&file_set, dir.path()).unwrap();
1141 assert!(repaired.success);
1142
1143 assert_eq!(
1144 std::fs::read(dir.path().join("file_a.bin")).unwrap(),
1145 file_a
1146 );
1147 assert_eq!(
1148 std::fs::read(dir.path().join("file_b.bin")).unwrap(),
1149 file_b
1150 );
1151 }
1152}