Skip to main content

rust_par2/
repair.rs

1//! PAR2 repair engine.
2//!
3//! Repairs damaged or missing files using Reed-Solomon recovery data.
4//!
5//! Algorithm (D×D reduced approach — D = damaged blocks, N = total blocks):
6//! 1. Verify to identify damaged/missing blocks
7//! 2. Load recovery blocks from volume files
8//! 3. Build a D×D Vandermonde submatrix (only damaged columns × recovery rows)
9//! 4. Invert the D×D matrix — O(D³) instead of O(N²D) for the full N×N
10//! 5. Compute adjusted recovery: subtract intact-block contributions from recovery data
11//! 6. Apply the D×D inverse to produce repaired blocks
12//! 7. Write repaired blocks back to files
13//!
14//! Steps 3–6 are the [`Repair`] builder, which works on slices in memory and
15//! does no I/O. The file-based functions ([`repair`], [`repair_from_verify`],
16//! [`repair_from_verify_no_reverify`]) read intact slices from disk, feed them
17//! to a [`Repair`], and write the solved slices back.
18//!
19//! # In-memory repair
20//!
21//! ```
22//! use rust_par2::recovery::RecoveryBlock;
23//! use rust_par2::repair::Repair;
24//! # fn demo(recovery: Vec<RecoveryBlock>, present: Vec<(usize, Vec<u8>)>) -> Result<(), rust_par2::RepairError> {
25//! // Global slice indices run across files in the Main packet's file order;
26//! // `BlockMap` maps them to (file, offset, length).
27//! let (slice_size, total_slices) = (768_000, 1_000);
28//!
29//! let mut r = Repair::new_owning(recovery, slice_size, total_slices)?;
30//! for (index, bytes) in &present {
31//!     // Check each slice against its IFSC checksum first: see `slice::SliceHasher`.
32//!     r.feed(*index, bytes)?;
33//! }
34//! let missing = [17, 18, 404];
35//! let rebuilt = r.solve(&missing)?; // index -> slice_size bytes (zero-padded)
36//! # let _ = rebuilt; Ok(()) }
37//! ```
38//!
39//! # Memory model
40//!
41//! A [`Repair`] holds one accumulator per recovery row: peak memory is
42//! `rows × slice_size`, plus `D × slice_size` for the solved output, plus
43//! whatever the caller buffers before feeding. Feeding allocates nothing for
44//! [`Repair::feed`]; [`Repair::feed_batch`] allocates one small coefficient
45//! vector per row per call. To bound memory, load or keep only as many recovery
46//! rows as there are missing slices ([`Repair::truncate_rows`] drops the rest at
47//! any time — each row is an independent equation).
48
49use 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/// Result of a repair operation.
66#[derive(Debug)]
67pub struct RepairResult {
68    /// Whether the repair succeeded (all files now intact).
69    pub success: bool,
70    /// Number of blocks repaired.
71    pub blocks_repaired: u32,
72    /// Number of files repaired.
73    pub files_repaired: usize,
74    /// Descriptive message.
75    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/// Errors that can occur during repair.
93#[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// ---------------------------------------------------------------------------
128// In-memory builder
129// ---------------------------------------------------------------------------
130
131/// Incremental, I/O-free PAR2 repair over slices held in memory.
132///
133/// Load recovery rows, [`feed`](Self::feed) every present input slice once,
134/// then [`solve`](Self::solve) for the missing ones. Each feed XOR-accumulates
135/// `c_i^exp_e × D_i` into every recovery row `e`, so the slices can arrive in
136/// any order and need not be kept after they are fed.
137///
138/// A fed slice cannot be removed. Feeding a corrupt slice makes every solved
139/// slice wrong, so verify slices first (see
140/// [`SliceHasher`](crate::slice::SliceHasher) and
141/// [`Par2File::slice_matches`](crate::Par2File::slice_matches)).
142///
143/// Slice indices are global: they run across the set's files in the Main
144/// packet's file order. [`BlockMap`] converts between global indices and file
145/// positions.
146#[derive(Debug, Clone)]
147pub struct Repair {
148    slice_size: usize,
149    /// PAR2 input constant for every global slice index.
150    constants: Vec<u16>,
151    /// Recovery exponent of each row, ascending.
152    exponents: Vec<u32>,
153    /// Accumulators, one per row: `R_e ⊕ Σ_fed c_i^exp_e × D_i`.
154    rows: Vec<Vec<u8>>,
155    fed: Vec<bool>,
156    fed_count: usize,
157}
158
159impl Repair {
160    /// Build from borrowed recovery blocks (their data is copied).
161    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    /// Build from owned recovery blocks, reusing their buffers as the row
170    /// accumulators.
171    ///
172    /// Rows are sorted by exponent and duplicate exponents dropped. Every block
173    /// must be exactly `slice_size` bytes.
174    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    /// Number of recovery rows loaded: the most missing slices `solve` can
204    /// rebuild.
205    pub fn rows(&self) -> usize {
206        self.rows.len()
207    }
208
209    /// Recovery exponents of the loaded rows, ascending.
210    pub fn exponents(&self) -> &[u32] {
211        &self.exponents
212    }
213
214    /// Slice size in bytes.
215    pub fn slice_size(&self) -> usize {
216        self.slice_size
217    }
218
219    /// Total number of input slices in the set.
220    pub fn total_slices(&self) -> usize {
221        self.fed.len()
222    }
223
224    /// Number of slices fed so far.
225    pub fn fed_count(&self) -> usize {
226        self.fed_count
227    }
228
229    /// Whether slice `index` has been fed.
230    pub fn is_fed(&self, index: usize) -> bool {
231        self.fed.get(index).copied().unwrap_or(false)
232    }
233
234    /// Keep only the first `n` rows (lowest exponents), freeing the rest.
235    ///
236    /// Safe at any point: each row is an independent equation. `solve` only
237    /// uses as many rows as there are missing slices, so once that number is
238    /// known the others are dead weight.
239    pub fn truncate_rows(&mut self, n: usize) {
240        self.rows.truncate(n);
241        self.exponents.truncate(n);
242    }
243
244    /// Feed one present input slice.
245    ///
246    /// `data` is the slice as stored: `slice_size` bytes, or fewer for the last
247    /// slice of a file, which is treated as zero-padded.
248    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    /// Feed several present slices at once.
261    ///
262    /// Each row is updated against the whole batch while it is hot in cache,
263    /// using the pair-batched SIMD path, so batches of a few dozen slices are
264    /// much faster than the same slices fed one by one. The batch is validated
265    /// before anything is accumulated: on error, nothing from it was fed.
266    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            // Marking as we go also rejects duplicates within the batch.
275            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    /// Rebuild the `missing` slices.
305    ///
306    /// Every slice must be either fed or listed in `missing`. Returns each
307    /// missing index mapped to its full `slice_size` bytes; the last slice of a
308    /// file comes back zero-padded, and the caller writes only its real length
309    /// (see [`BlockMap::global_to_file`]).
310    ///
311    /// Uses the first `missing.len()` rows. Errors with
312    /// [`RepairError::InsufficientRecovery`] if there are fewer rows than
313    /// missing slices, and [`RepairError::SingularMatrix`] if the chosen rows
314    /// are linearly dependent.
315    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    /// Solve for sorted, deduplicated `missing`; outputs are in the same order.
342    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        // Rearranging the encoding equation R_e = Σ_i c_i^exp_e × D_i for the
369        // missing slices only:
370        //   R_e ⊕ Σ_{fed i} c_i^exp_e × D_i = Σ_{missing j} c_j^exp_e × D_j
371        // The left side is what the rows hold. Invert V[e][j] = c_j^exp_e.
372        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        // repaired[j] = Σ_e V⁻¹[j][e] × row[e]
382        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
392/// `dst ^= c × src` over `src.len()` bytes, `src` treated as zero-padded to
393/// `dst.len()`. Padding contributes nothing, so only the prefix is touched; an
394/// odd trailing byte is the low half of a GF(2^16) word whose high byte is 0.
395fn 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
405// ---------------------------------------------------------------------------
406// File-based repair
407// ---------------------------------------------------------------------------
408
409/// Repair damaged/missing files in a PAR2 set.
410///
411/// This is a blocking operation. For async contexts, wrap in `spawn_blocking`.
412///
413/// Runs verification internally to identify damage. If you already have a
414/// [`VerifyResult`] from a prior [`verify()`](crate::verify) call, use
415/// [`repair_from_verify`] instead to skip the redundant verification pass.
416pub 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
421/// Repair using a pre-computed [`VerifyResult`].
422///
423/// This skips the initial verification pass, saving significant time when the
424/// caller has already called [`verify()`](crate::verify). The `verify_result`
425/// must have been computed against the same `file_set` and `dir`.
426///
427/// Uses a reduced D×D matrix approach where D = number of damaged blocks.
428/// The full N×N decode matrix is never constructed — only a small Vandermonde
429/// submatrix covering damaged positions is built and inverted. This reduces
430/// matrix inversion from O(N²D) to O(D³), which is the difference between
431/// minutes and milliseconds for typical repair scenarios.
432///
433/// After repair, a full MD5 verification pass confirms all files are correct.
434/// To skip re-verification, use [`repair_from_verify_no_reverify`].
435///
436/// This is a blocking operation. For async contexts, wrap in `spawn_blocking`.
437pub 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
445/// Like [`repair_from_verify`], but skips re-verification after repair.
446///
447/// The Reed-Solomon math is deterministic — if the matrix inverted and I/O
448/// succeeded, the output is correct. Skipping re-verify saves a full read
449/// of all files (~6s per 5GB).
450pub 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    // Step 1: Load recovery blocks from all volume files
477    let recovery_blocks = load_recovery_blocks(dir, &file_set.recovery_set_id, file_set.slice_size);
478
479    // Step 2: Map files to global block indices
480    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    // Exactly D recovery rows are needed (one per damaged block).
499    fold.truncate_rows(damaged_indices.len());
500
501    // Step 3: Fold intact blocks into the recovery rows.
502    //
503    // Cache-tiled: the D row buffers (D × slice_size) typically exceed L3.
504    // Iterating source-major thrashes cache on every source. Instead we read
505    // batches of B source blocks and feed_batch processes each row against the
506    // whole batch, keeping the row hot in L1/L2 while applying B sources via
507    // mul_add_multi's pair-batched AVX2 path.
508    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    // Batch size: chosen so B × slice_size fits comfortably in L3 cache (~16-32MB).
514    // With 768KB slices, 24 blocks = ~18MB. Leaves room for output buffer in L1/L2.
515    const BATCH_SIZE: usize = 24;
516
517    // I/O is sequential so OS readahead keeps the pipeline full.
518    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    // Steps 4–5: invert the D×D matrix and apply it.
536    let mut repaired = fold.solve(&damaged_indices)?;
537    drop(fold);
538
539    info!("Repaired blocks reconstructed via D×D inverse");
540
541    // Step 6: Write repaired blocks back to files
542    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        // Ensure file is the right size (for missing files)
569        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    // Step 7: Optional re-verify
581    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// ---------------------------------------------------------------------------
614// Block mapping
615// ---------------------------------------------------------------------------
616
617/// Maps between global slice indices and per-file slice positions.
618///
619/// Global indices run across the set's files in the Main packet's file order
620/// (falling back to File ID order if the Main packet listed none); this is the
621/// order the PAR2 input constants are assigned in.
622#[derive(Debug, Clone)]
623pub struct BlockMap {
624    /// Files in global order.
625    pub files: Vec<BlockFile>,
626    /// Total number of input slices across all files.
627    pub total_blocks: u32,
628    /// Slice size in bytes.
629    pub slice_size: u64,
630}
631
632/// One file's place in a [`BlockMap`].
633#[derive(Debug, Clone)]
634pub struct BlockFile {
635    /// File ID from the FileDesc packet.
636    pub file_id: Id16,
637    /// Filename from the FileDesc packet.
638    pub filename: String,
639    /// File size in bytes.
640    pub file_size: u64,
641    /// Number of slices in this file.
642    pub block_count: u32,
643    /// First global slice index for this file.
644    pub start_block: u32,
645}
646
647/// Where a global slice lives on disk. Returned by [`BlockMap::global_to_file`].
648#[derive(Debug, Clone, Copy)]
649pub struct SliceLocation<'a> {
650    /// The file holding the slice.
651    pub file: &'a BlockFile,
652    /// Slice index within the file (0-based).
653    pub local_index: u32,
654    /// Byte offset of the slice within the file.
655    pub offset: u64,
656    /// Bytes of real data: `slice_size`, or less for the file's last slice.
657    pub len: usize,
658}
659
660/// Build the global slice map for a file set.
661pub 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    /// Build the global slice map for a file set. Same as [`build_block_map`].
703    pub fn new(file_set: &Par2FileSet) -> Self {
704        build_block_map(file_set)
705    }
706
707    /// Locate a global slice index: its file, byte offset and real length.
708    /// `None` if the index is out of range.
709    pub fn global_to_file(&self, global_idx: usize) -> Option<SliceLocation<'_>> {
710        let global = u32::try_from(global_idx).ok()?;
711        // Files are in start_block order; empty files end where they start and
712        // are skipped.
713        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    /// Global index of slice `local_index` of the file named `filename`.
732    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    /// The entry for the file named `filename`.
738    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                // No per-block info — assume all blocks damaged
750                for i in 0..bf.block_count {
751                    indices.push((bf.start_block + i) as usize);
752                }
753            } else {
754                // Use precise per-block damage info
755                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
775/// Read a single source block from disk, reusing file handles.
776fn 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]; // zero-initialized for last-block padding
797    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 that the basic RS encode→decode round-trip works with the D×D approach.
942    /// 2 data blocks, 2 recovery blocks, lose both data blocks, recover.
943    #[test]
944    fn test_rs_roundtrip_simple() {
945        // 2 input "blocks" of 4 bytes each (2 u16 values)
946        let input0: Vec<u8> = vec![0x01, 0x00, 0x02, 0x00]; // [1, 2] as u16 LE
947        let input1: Vec<u8> = vec![0x03, 0x00, 0x04, 0x00]; // [3, 4] as u16 LE
948
949        let input_count = 2;
950        let recovery_exponents = vec![0u32, 1u32];
951
952        // Build encoding matrix to compute recovery blocks
953        let enc = GfMatrix::par2_encoding_matrix(input_count, &recovery_exponents);
954
955        // Compute recovery blocks
956        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        // Now "lose" both input blocks. Use D×D approach to recover.
979        // D = 2 (both blocks damaged), N = 2.
980        // Since all blocks are damaged, there are no intact blocks to subtract.
981        // adjusted[e] = recovery[e] (no intact contributions to remove).
982
983        let constants = par2_input_constants(input_count);
984        let damaged_indices = [0usize, 1usize];
985
986        // Build D×D Vandermonde: V[e][j] = c_{damaged_j}^{exp_e}
987        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        // Apply inverse: repaired[j] = Σ_e inv[j][e] * adjusted[e]
998        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 D×D approach with partial damage (some blocks intact).
1022    /// 4 data blocks, lose 2, recover using 2 recovery blocks.
1023    #[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        // Compute recovery blocks
1039        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        // "Lose" blocks 1 and 3. Blocks 0 and 2 are intact.
1054        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        // Build D×D Vandermonde
1063        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        // Compute adjusted recovery: subtract intact contributions
1072        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        // Apply inverse
1082        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        // Verify recovery
1091        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}