1use std::collections::HashMap;
15use std::collections::hash_map::Entry;
16use std::fmt;
17use std::io::{Read, Seek, SeekFrom, Write};
18use std::path::Path;
19
20use rayon::prelude::*;
21use tracing::{debug, info};
22
23use crate::gf;
24use crate::gf_simd;
25use crate::matrix::{GfMatrix, par2_input_constants};
26use crate::recovery::{RecoveryBlock, load_recovery_blocks};
27use crate::types::{Par2FileSet, VerifyResult};
28use crate::verify;
29
30#[derive(Debug)]
32pub struct RepairResult {
33 pub success: bool,
35 pub blocks_repaired: u32,
37 pub files_repaired: usize,
39 pub message: String,
41}
42
43impl fmt::Display for RepairResult {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 if self.success {
46 write!(
47 f,
48 "Repair complete: {} blocks repaired across {} files",
49 self.blocks_repaired, self.files_repaired
50 )
51 } else {
52 write!(f, "Repair failed: {}", self.message)
53 }
54 }
55}
56
57#[derive(Debug, thiserror::Error)]
59pub enum RepairError {
60 #[error("I/O error: {0}")]
61 Io(#[from] std::io::Error),
62 #[error("Insufficient recovery data: need {needed} blocks, have {available}")]
63 InsufficientRecovery { needed: u32, available: u32 },
64 #[error("Decode matrix is singular — cannot repair with these recovery blocks")]
65 SingularMatrix,
66 #[error("No damage detected — nothing to repair")]
67 NoDamage,
68 #[error("Verification after repair failed: {0}")]
69 VerifyFailed(String),
70}
71
72pub fn repair(file_set: &Par2FileSet, dir: &Path) -> Result<RepairResult, RepairError> {
80 let verify_result = verify::verify(file_set, dir);
81 repair_from_verify_inner(file_set, dir, &verify_result, true)
82}
83
84pub fn repair_from_verify(
101 file_set: &Par2FileSet,
102 dir: &Path,
103 verify_result: &VerifyResult,
104) -> Result<RepairResult, RepairError> {
105 repair_from_verify_inner(file_set, dir, verify_result, true)
106}
107
108pub fn repair_from_verify_no_reverify(
114 file_set: &Par2FileSet,
115 dir: &Path,
116 verify_result: &VerifyResult,
117) -> Result<RepairResult, RepairError> {
118 repair_from_verify_inner(file_set, dir, verify_result, false)
119}
120
121fn repair_from_verify_inner(
122 file_set: &Par2FileSet,
123 dir: &Path,
124 verify_result: &VerifyResult,
125 re_verify: bool,
126) -> Result<RepairResult, RepairError> {
127 if verify_result.all_correct() {
128 return Err(RepairError::NoDamage);
129 }
130
131 let blocks_needed = verify_result.blocks_needed();
132 info!(
133 blocks_needed,
134 damaged = verify_result.damaged.len(),
135 missing = verify_result.missing.len(),
136 "Repair: damage detected"
137 );
138
139 let recovery_blocks = load_recovery_blocks(dir, &file_set.recovery_set_id, file_set.slice_size);
141
142 if (recovery_blocks.len() as u32) < blocks_needed {
143 return Err(RepairError::InsufficientRecovery {
144 needed: blocks_needed,
145 available: recovery_blocks.len() as u32,
146 });
147 }
148
149 let block_map = build_block_map(file_set);
151 let total_input_blocks = block_map.total_blocks as usize;
152
153 let damaged_indices = find_damaged_block_indices(verify_result, &block_map);
154 let num_damaged = damaged_indices.len();
155 info!(
156 damaged_block_count = num_damaged,
157 total_input_blocks, "Mapped damaged blocks to global indices"
158 );
159
160 let recovery_to_use: Vec<&RecoveryBlock> = recovery_blocks.iter().take(num_damaged).collect();
162 let recovery_exponents: Vec<u32> = recovery_to_use.iter().map(|b| b.exponent).collect();
163
164 let constants = par2_input_constants(total_input_blocks);
174
175 let mut vandermonde = GfMatrix::zeros(num_damaged, num_damaged);
176 for (e, &exp) in recovery_exponents.iter().enumerate() {
177 for (j, &dmg_idx) in damaged_indices.iter().enumerate() {
178 vandermonde.set(e, j, gf::pow(constants[dmg_idx], exp));
179 }
180 }
181
182 let inverse = vandermonde.invert().ok_or(RepairError::SingularMatrix)?;
183
184 info!(
185 "D×D decode matrix inverted ({}×{})",
186 num_damaged, num_damaged
187 );
188
189 let slice_size = file_set.slice_size as usize;
204
205 let damaged_set: std::collections::HashSet<usize> = damaged_indices.iter().copied().collect();
207
208 let mut adjusted: Vec<Vec<u8>> = recovery_to_use.iter().map(|rb| rb.data.clone()).collect();
210
211 let intact_indices: Vec<usize> = (0..total_input_blocks)
213 .filter(|i| !damaged_set.contains(i))
214 .collect();
215
216 const BATCH_SIZE: usize = 24;
219
220 let mut file_handles: HashMap<String, std::fs::File> = HashMap::new();
223
224 for batch in intact_indices.chunks(BATCH_SIZE) {
225 let batch_data: Vec<Vec<u8>> = batch
227 .iter()
228 .map(|&idx| read_source_block(dir, &block_map, idx, slice_size, &mut file_handles))
229 .collect::<std::io::Result<Vec<_>>>()?;
230
231 let batch_refs: Vec<&[u8]> = batch_data.iter().map(|v| v.as_slice()).collect();
232
233 adjusted
237 .par_iter_mut()
238 .enumerate()
239 .for_each(|(e, adj_buf)| {
240 let coeffs: Vec<u16> = batch
241 .iter()
242 .map(|&src_idx| gf::pow(constants[src_idx], recovery_exponents[e]))
243 .collect();
244 gf_simd::mul_add_multi(adj_buf, &batch_refs, &coeffs);
245 });
246 }
247
248 info!("Intact-block contributions subtracted from recovery data");
249
250 let adj_refs: Vec<&[u8]> = adjusted.iter().map(|v| v.as_slice()).collect();
257
258 let mut outputs: Vec<Vec<u8>> = (0..num_damaged).map(|_| vec![0u8; slice_size]).collect();
259
260 outputs.par_iter_mut().enumerate().for_each(|(j, dst)| {
261 let coeffs: Vec<u16> = (0..num_damaged).map(|e| inverse.get(j, e)).collect();
262 gf_simd::mul_add_multi(dst, &adj_refs, &coeffs);
263 });
264
265 info!("Repaired blocks reconstructed via D×D inverse");
266
267 let repaired_blocks: Vec<(usize, Vec<u8>)> =
268 damaged_indices.iter().copied().zip(outputs).collect();
269
270 let mut files_touched = std::collections::HashSet::new();
272
273 for (global_idx, data) in &repaired_blocks {
274 let (filename, file_offset, write_len) = block_map.global_to_file(*global_idx, slice_size);
275
276 let file_path = dir.join(&filename);
277 debug!(
278 filename,
279 global_block = global_idx,
280 offset = file_offset,
281 len = write_len,
282 "Writing repaired block"
283 );
284
285 let mut f = std::fs::OpenOptions::new()
286 .create(true)
287 .truncate(false)
288 .write(true)
289 .open(&file_path)?;
290
291 let expected_size = block_map
293 .files
294 .iter()
295 .find(|bf| bf.filename == filename)
296 .map(|bf| bf.file_size)
297 .unwrap_or(0);
298 let current_size = f.metadata()?.len();
299 if current_size < expected_size {
300 f.set_len(expected_size)?;
301 }
302
303 f.seek(SeekFrom::Start(file_offset as u64))?;
304 f.write_all(&data[..write_len])?;
305 files_touched.insert(filename.clone());
306 }
307
308 if re_verify {
310 let verification = verify::verify(file_set, dir);
311 if verification.all_correct() {
312 info!(
313 blocks = repaired_blocks.len(),
314 files = files_touched.len(),
315 "Repair successful — all files verified"
316 );
317 Ok(RepairResult {
318 success: true,
319 blocks_repaired: repaired_blocks.len() as u32,
320 files_repaired: files_touched.len(),
321 message: "All files repaired and verified".to_string(),
322 })
323 } else {
324 Err(RepairError::VerifyFailed(format!("{verification}")))
325 }
326 } else {
327 info!(
328 blocks = repaired_blocks.len(),
329 files = files_touched.len(),
330 "Repair complete (re-verify skipped)"
331 );
332 Ok(RepairResult {
333 success: true,
334 blocks_repaired: repaired_blocks.len() as u32,
335 files_repaired: files_touched.len(),
336 message: "All files repaired (re-verify skipped)".to_string(),
337 })
338 }
339}
340
341struct BlockMap {
347 files: Vec<BlockFile>,
348 total_blocks: u32,
349}
350
351struct BlockFile {
352 filename: String,
353 file_size: u64,
354 block_count: u32,
355 start_block: u32,
357}
358
359fn build_block_map(file_set: &Par2FileSet) -> BlockMap {
360 let slice_size = file_set.slice_size;
361 let mut files = Vec::new();
362 let mut block_offset = 0u32;
363
364 let ordered_files: Vec<_> = if file_set.file_order.is_empty() {
365 let mut fallback: Vec<_> = file_set.files.values().collect();
366 fallback.sort_by_key(|f| f.file_id);
367 fallback
368 } else {
369 file_set
370 .file_order
371 .iter()
372 .filter_map(|id| file_set.files.get(id))
373 .collect()
374 };
375
376 for f in ordered_files {
377 let block_count = if slice_size == 0 {
378 0
379 } else {
380 f.size.div_ceil(slice_size) as u32
381 };
382 files.push(BlockFile {
383 filename: f.filename.clone(),
384 file_size: f.size,
385 block_count,
386 start_block: block_offset,
387 });
388 block_offset += block_count;
389 }
390
391 BlockMap {
392 files,
393 total_blocks: block_offset,
394 }
395}
396
397impl BlockMap {
398 fn global_to_file(&self, global_idx: usize, slice_size: usize) -> (String, usize, usize) {
400 let global = global_idx as u32;
401 for f in &self.files {
402 if global >= f.start_block && global < f.start_block + f.block_count {
403 let local_block = (global - f.start_block) as usize;
404 let file_offset = local_block * slice_size;
405 let remaining = f.file_size as usize - file_offset;
407 let write_len = remaining.min(slice_size);
408 return (f.filename.clone(), file_offset, write_len);
409 }
410 }
411 panic!("Global block index {global_idx} out of range");
412 }
413}
414
415fn find_damaged_block_indices(verify_result: &VerifyResult, block_map: &BlockMap) -> Vec<usize> {
416 let mut indices = Vec::new();
417
418 for damaged in &verify_result.damaged {
419 if let Some(bf) = block_map
420 .files
421 .iter()
422 .find(|f| f.filename == damaged.filename)
423 {
424 if damaged.damaged_block_indices.is_empty() {
425 for i in 0..bf.block_count {
427 indices.push((bf.start_block + i) as usize);
428 }
429 } else {
430 for &local_idx in &damaged.damaged_block_indices {
432 indices.push((bf.start_block + local_idx) as usize);
433 }
434 }
435 }
436 }
437
438 for missing in &verify_result.missing {
439 if let Some(bf) = block_map
440 .files
441 .iter()
442 .find(|f| f.filename == missing.filename)
443 {
444 for i in 0..bf.block_count {
445 indices.push((bf.start_block + i) as usize);
446 }
447 }
448 }
449
450 indices.sort();
451 indices.dedup();
452 indices
453}
454
455fn read_source_block(
457 dir: &Path,
458 block_map: &BlockMap,
459 global_idx: usize,
460 slice_size: usize,
461 file_handles: &mut HashMap<String, std::fs::File>,
462) -> std::io::Result<Vec<u8>> {
463 let (filename, file_offset, _) = block_map.global_to_file(global_idx, slice_size);
464
465 let handle = match file_handles.entry(filename.clone()) {
466 Entry::Occupied(e) => e.into_mut(),
467 Entry::Vacant(e) => {
468 let path = dir.join(&filename);
469 e.insert(std::fs::File::open(&path)?)
470 }
471 };
472 handle.seek(SeekFrom::Start(file_offset as u64))?;
473
474 let mut buf = vec![0u8; slice_size]; let mut total = 0;
476 while total < slice_size {
477 match handle.read(&mut buf[total..]) {
478 Ok(0) => break,
479 Ok(n) => total += n,
480 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
481 Err(e) => return Err(e),
482 }
483 }
484 Ok(buf)
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use std::path::PathBuf;
491
492 use crc32fast::hash as crc32_hash;
493 use md5::{Digest, Md5};
494
495 use crate::packets::{HEADER_SIZE, MAGIC};
496
497 const TYPE_MAIN: &[u8; 16] = b"PAR 2.0\x00Main\x00\x00\x00\x00";
498 const TYPE_FILE_DESC: &[u8; 16] = b"PAR 2.0\x00FileDesc";
499 const TYPE_IFSC: &[u8; 16] = b"PAR 2.0\x00IFSC\x00\x00\x00\x00";
500 const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
501
502 fn build_packet(set_id: [u8; 16], packet_type: &[u8; 16], body: &[u8]) -> Vec<u8> {
503 let mut data = Vec::with_capacity(32 + body.len());
504 data.extend_from_slice(&set_id);
505 data.extend_from_slice(packet_type);
506 data.extend_from_slice(body);
507
508 let mut packet = Vec::with_capacity(32 + data.len());
509 packet.extend_from_slice(MAGIC);
510 packet.extend_from_slice(&((HEADER_SIZE + body.len()) as u64).to_le_bytes());
511 packet.extend_from_slice(&Md5::digest(&data));
512 packet.extend_from_slice(&data);
513 packet
514 }
515
516 fn file_md5(data: &[u8]) -> [u8; 16] {
517 Md5::digest(data).into()
518 }
519
520 fn hash_16k(data: &[u8]) -> [u8; 16] {
521 Md5::digest(&data[..data.len().min(16_384)]).into()
522 }
523
524 fn padded_block(data: &[u8], block_index: usize, slice_size: usize) -> Vec<u8> {
525 let start = block_index * slice_size;
526 let end = (start + slice_size).min(data.len());
527 let mut block = vec![0u8; slice_size];
528 if start < data.len() {
529 block[..end - start].copy_from_slice(&data[start..end]);
530 }
531 block
532 }
533
534 fn write_par2_fixture(
535 dir: &Path,
536 slice_size: usize,
537 files: &[(&str, [u8; 16], Vec<u8>)],
538 main_order: &[[u8; 16]],
539 recovery_exponents: &[u32],
540 ) -> PathBuf {
541 let set_id = [0x5Au8; 16];
542 let mut index_bytes = Vec::new();
543
544 let mut main_body = Vec::new();
545 main_body.extend_from_slice(&(slice_size as u64).to_le_bytes());
546 main_body.extend_from_slice(&(main_order.len() as u32).to_le_bytes());
547 for file_id in main_order {
548 main_body.extend_from_slice(file_id);
549 }
550 index_bytes.extend_from_slice(&build_packet(set_id, TYPE_MAIN, &main_body));
551
552 for (filename, file_id, data) in files {
553 std::fs::write(dir.join(filename), data).unwrap();
554
555 let mut desc_body = Vec::new();
556 desc_body.extend_from_slice(file_id);
557 desc_body.extend_from_slice(&file_md5(data));
558 desc_body.extend_from_slice(&hash_16k(data));
559 desc_body.extend_from_slice(&(data.len() as u64).to_le_bytes());
560 desc_body.extend_from_slice(filename.as_bytes());
561 desc_body.push(0);
562 while desc_body.len() % 4 != 0 {
563 desc_body.push(0);
564 }
565 index_bytes.extend_from_slice(&build_packet(set_id, TYPE_FILE_DESC, &desc_body));
566
567 let block_count = data.len().div_ceil(slice_size);
568 let mut ifsc_body = Vec::new();
569 ifsc_body.extend_from_slice(file_id);
570 for block_idx in 0..block_count {
571 let block = padded_block(data, block_idx, slice_size);
572 ifsc_body.extend_from_slice(&file_md5(&block));
573 ifsc_body.extend_from_slice(&crc32_hash(&block).to_le_bytes());
574 }
575 index_bytes.extend_from_slice(&build_packet(set_id, TYPE_IFSC, &ifsc_body));
576 }
577
578 std::fs::write(dir.join("fixture.par2"), &index_bytes).unwrap();
579
580 let ordered_files: Vec<_> = main_order
581 .iter()
582 .map(|wanted| {
583 files
584 .iter()
585 .find(|(_, file_id, _)| file_id == wanted)
586 .unwrap()
587 })
588 .collect();
589 let input_blocks: Vec<Vec<u8>> = ordered_files
590 .iter()
591 .flat_map(|(_, _, data)| {
592 let count = data.len().div_ceil(slice_size);
593 (0..count).map(move |block_idx| padded_block(data, block_idx, slice_size))
594 })
595 .collect();
596
597 let input_count = input_blocks.len();
598 let enc = GfMatrix::par2_encoding_matrix(input_count, recovery_exponents);
599 let srcs: Vec<&[u8]> = input_blocks.iter().map(Vec::as_slice).collect();
600 let mut vol_bytes = index_bytes.clone();
601
602 for (row_idx, &exp) in recovery_exponents.iter().enumerate() {
603 let coeffs: Vec<u16> = (0..input_count)
604 .map(|col| enc.get(input_count + row_idx, col))
605 .collect();
606 let mut recovery = vec![0u8; slice_size];
607 gf_simd::mul_add_multi(&mut recovery, &srcs, &coeffs);
608
609 let mut body = Vec::with_capacity(4 + slice_size);
610 body.extend_from_slice(&exp.to_le_bytes());
611 body.extend_from_slice(&recovery);
612 vol_bytes.extend_from_slice(&build_packet(set_id, TYPE_RECOVERY, &body));
613 }
614
615 std::fs::write(dir.join("fixture.vol00+2.par2"), &vol_bytes).unwrap();
616 dir.join("fixture.par2")
617 }
618
619 #[test]
622 fn test_rs_roundtrip_simple() {
623 let input0: Vec<u8> = vec![0x01, 0x00, 0x02, 0x00]; let input1: Vec<u8> = vec![0x03, 0x00, 0x04, 0x00]; let input_count = 2;
628 let recovery_exponents = vec![0u32, 1u32];
629
630 let enc = GfMatrix::par2_encoding_matrix(input_count, &recovery_exponents);
632
633 let slice_size = 4;
635 let u16_per_slice = slice_size / 2;
636 let inputs = [&input0, &input1];
637
638 let mut recovery0 = vec![0u8; slice_size];
639 let mut recovery1 = vec![0u8; slice_size];
640
641 for pos in 0..u16_per_slice {
642 let off = pos * 2;
643 let mut r0: u16 = 0;
644 let mut r1: u16 = 0;
645 for (i, inp) in inputs.iter().enumerate() {
646 let val = u16::from_le_bytes([inp[off], inp[off + 1]]);
647 r0 = gf::add(r0, gf::mul(enc.get(2, i), val));
648 r1 = gf::add(r1, gf::mul(enc.get(3, i), val));
649 }
650 recovery0[off] = r0 as u8;
651 recovery0[off + 1] = (r0 >> 8) as u8;
652 recovery1[off] = r1 as u8;
653 recovery1[off + 1] = (r1 >> 8) as u8;
654 }
655
656 let constants = par2_input_constants(input_count);
662 let damaged_indices = [0usize, 1usize];
663
664 let num_damaged = damaged_indices.len();
666 let mut vandermonde = GfMatrix::zeros(num_damaged, num_damaged);
667 for (e, &exp) in recovery_exponents.iter().enumerate() {
668 for (j, &dmg_idx) in damaged_indices.iter().enumerate() {
669 vandermonde.set(e, j, gf::pow(constants[dmg_idx], exp));
670 }
671 }
672
673 let inv = vandermonde.invert().expect("Should be invertible");
674
675 let adjusted = [&recovery0[..], &recovery1[..]];
677 let mut result0 = vec![0u8; slice_size];
678 let mut result1 = vec![0u8; slice_size];
679
680 for pos in 0..u16_per_slice {
681 let off = pos * 2;
682 let mut out0: u16 = 0;
683 let mut out1: u16 = 0;
684 for (e, adj) in adjusted.iter().enumerate() {
685 let val = u16::from_le_bytes([adj[off], adj[off + 1]]);
686 out0 = gf::add(out0, gf::mul(inv.get(0, e), val));
687 out1 = gf::add(out1, gf::mul(inv.get(1, e), val));
688 }
689 result0[off] = out0 as u8;
690 result0[off + 1] = (out0 >> 8) as u8;
691 result1[off] = out1 as u8;
692 result1[off + 1] = (out1 >> 8) as u8;
693 }
694
695 assert_eq!(result0, input0, "Recovered block 0 should match original");
696 assert_eq!(result1, input1, "Recovered block 1 should match original");
697 }
698
699 #[test]
702 fn test_rs_roundtrip_partial_damage() {
703 let slice_size = 4;
704 let input_count = 4;
705 let recovery_exponents = vec![0u32, 1u32];
706
707 let inputs: Vec<Vec<u8>> = vec![
708 vec![0x01, 0x00, 0x02, 0x00],
709 vec![0x03, 0x00, 0x04, 0x00],
710 vec![0x05, 0x00, 0x06, 0x00],
711 vec![0x07, 0x00, 0x08, 0x00],
712 ];
713
714 let enc = GfMatrix::par2_encoding_matrix(input_count, &recovery_exponents);
715
716 let mut recovery = vec![vec![0u8; slice_size]; 2];
718 for pos in 0..(slice_size / 2) {
719 let off = pos * 2;
720 for (e, rec) in recovery.iter_mut().enumerate() {
721 let mut val: u16 = 0;
722 for (i, inp) in inputs.iter().enumerate() {
723 let d = u16::from_le_bytes([inp[off], inp[off + 1]]);
724 val = gf::add(val, gf::mul(enc.get(input_count + e, i), d));
725 }
726 rec[off] = val as u8;
727 rec[off + 1] = (val >> 8) as u8;
728 }
729 }
730
731 let damaged_indices = [1usize, 3usize];
733 let intact_indices: Vec<usize> = (0..input_count)
734 .filter(|i| !damaged_indices.contains(i))
735 .collect();
736 let num_damaged = damaged_indices.len();
737
738 let constants = par2_input_constants(input_count);
739
740 let mut vandermonde = GfMatrix::zeros(num_damaged, num_damaged);
742 for (e, &exp) in recovery_exponents.iter().enumerate() {
743 for (j, &dmg_idx) in damaged_indices.iter().enumerate() {
744 vandermonde.set(e, j, gf::pow(constants[dmg_idx], exp));
745 }
746 }
747 let inv = vandermonde.invert().expect("Should be invertible");
748
749 let mut adjusted = recovery.clone();
751 for &intact_idx in &intact_indices {
752 let c_i = constants[intact_idx];
753 for (e, adj) in adjusted.iter_mut().enumerate() {
754 let coeff = gf::pow(c_i, recovery_exponents[e]);
755 gf_simd::mul_add_buffer(adj, &inputs[intact_idx], coeff);
756 }
757 }
758
759 let adj_refs: Vec<&[u8]> = adjusted.iter().map(|v| v.as_slice()).collect();
761 let mut outputs: Vec<Vec<u8>> = (0..num_damaged).map(|_| vec![0u8; slice_size]).collect();
762
763 for (j, dst) in outputs.iter_mut().enumerate() {
764 let coeffs: Vec<u16> = (0..num_damaged).map(|e| inv.get(j, e)).collect();
765 gf_simd::mul_add_multi(dst, &adj_refs, &coeffs);
766 }
767
768 assert_eq!(outputs[0], inputs[1], "Recovered block 1 should match");
770 assert_eq!(outputs[1], inputs[3], "Recovered block 3 should match");
771 }
772
773 #[test]
774 fn test_repair_cross_file_damage_respects_main_packet_file_order() {
775 let dir = tempfile::tempdir().unwrap();
776 let slice_size = 4096usize;
777 let file_a_id = [0x20; 16];
778 let file_b_id = [0x10; 16];
779 let file_a = vec![0x41; slice_size * 2];
780 let file_b = vec![0x42; slice_size * 2];
781
782 let par2_path = write_par2_fixture(
783 dir.path(),
784 slice_size,
785 &[
786 ("file_a.bin", file_a_id, file_a.clone()),
787 ("file_b.bin", file_b_id, file_b.clone()),
788 ],
789 &[file_a_id, file_b_id],
790 &[0, 1],
791 );
792
793 let file_set = crate::parse(&par2_path).unwrap();
794 assert_eq!(file_set.file_order, vec![file_a_id, file_b_id]);
795
796 {
797 let file_a_path = dir.path().join("file_a.bin");
798 let file_b_path = dir.path().join("file_b.bin");
799 let mut a = std::fs::OpenOptions::new()
800 .write(true)
801 .open(&file_a_path)
802 .unwrap();
803 let mut b = std::fs::OpenOptions::new()
804 .write(true)
805 .open(&file_b_path)
806 .unwrap();
807 a.seek(SeekFrom::Start(slice_size as u64)).unwrap();
808 b.seek(SeekFrom::Start(slice_size as u64)).unwrap();
809 a.write_all(&vec![0xDE; slice_size]).unwrap();
810 b.write_all(&vec![0xAD; slice_size]).unwrap();
811 }
812
813 let pre = verify::verify(&file_set, dir.path());
814 assert_eq!(pre.damaged.len(), 2);
815 assert_eq!(pre.blocks_needed(), 2);
816 assert!(pre.repair_possible);
817
818 let repaired = repair(&file_set, dir.path()).unwrap();
819 assert!(repaired.success);
820
821 assert_eq!(
822 std::fs::read(dir.path().join("file_a.bin")).unwrap(),
823 file_a
824 );
825 assert_eq!(
826 std::fs::read(dir.path().join("file_b.bin")).unwrap(),
827 file_b
828 );
829 }
830}