use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use rayon::prelude::*;
use tracing::{debug, info};
use crate::gf;
use crate::gf_simd;
use crate::matrix::{GfMatrix, par2_input_constants};
use crate::recovery::{RecoveryBlock, load_recovery_blocks};
use crate::types::{Id16, Par2FileSet, VerifyResult};
use crate::verify;
#[derive(Debug)]
pub struct RepairResult {
pub success: bool,
pub blocks_repaired: u32,
pub files_repaired: usize,
pub message: String,
}
impl fmt::Display for RepairResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.success {
write!(
f,
"Repair complete: {} blocks repaired across {} files",
self.blocks_repaired, self.files_repaired
)
} else {
write!(f, "Repair failed: {}", self.message)
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum RepairError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Insufficient recovery data: need {needed} blocks, have {available}")]
InsufficientRecovery { needed: u32, available: u32 },
#[error("Decode matrix is singular — cannot repair with these recovery blocks")]
SingularMatrix,
#[error("No damage detected — nothing to repair")]
NoDamage,
#[error("Verification after repair failed: {0}")]
VerifyFailed(String),
#[error("Invalid slice size {0}: must be a non-zero multiple of 4")]
InvalidSliceSize(usize),
#[error("Recovery block {exponent} is {len} bytes, expected the slice size {slice_size}")]
InvalidRecoveryBlock {
exponent: u32,
len: usize,
slice_size: usize,
},
#[error("Slice index {index} out of range: the set has {total} input slices")]
SliceOutOfRange { index: usize, total: usize },
#[error("Slice {index} is {len} bytes, longer than the slice size {slice_size}")]
SliceTooLong {
index: usize,
len: usize,
slice_size: usize,
},
#[error("Slice {index} was already fed (feeding twice cancels it out)")]
DuplicateSlice { index: usize },
#[error("{unaccounted} input slices were neither fed nor listed as missing")]
IncompleteFeed { unaccounted: usize },
}
#[derive(Debug, Clone)]
pub struct Repair {
slice_size: usize,
constants: Vec<u16>,
exponents: Vec<u32>,
rows: Vec<Vec<u8>>,
fed: Vec<bool>,
fed_count: usize,
}
impl Repair {
pub fn new(
recovery: &[RecoveryBlock],
slice_size: usize,
total_input_slices: usize,
) -> Result<Self, RepairError> {
Self::new_owning(recovery.to_vec(), slice_size, total_input_slices)
}
pub fn new_owning(
mut recovery: Vec<RecoveryBlock>,
slice_size: usize,
total_input_slices: usize,
) -> Result<Self, RepairError> {
if slice_size == 0 || slice_size % 4 != 0 {
return Err(RepairError::InvalidSliceSize(slice_size));
}
if let Some(bad) = recovery.iter().find(|b| b.data.len() != slice_size) {
return Err(RepairError::InvalidRecoveryBlock {
exponent: bad.exponent,
len: bad.data.len(),
slice_size,
});
}
recovery.sort_by_key(|b| b.exponent);
recovery.dedup_by_key(|b| b.exponent);
let (exponents, rows) = recovery.into_iter().map(|b| (b.exponent, b.data)).unzip();
Ok(Self {
slice_size,
constants: par2_input_constants(total_input_slices),
exponents,
rows,
fed: vec![false; total_input_slices],
fed_count: 0,
})
}
pub fn rows(&self) -> usize {
self.rows.len()
}
pub fn exponents(&self) -> &[u32] {
&self.exponents
}
pub fn slice_size(&self) -> usize {
self.slice_size
}
pub fn total_slices(&self) -> usize {
self.fed.len()
}
pub fn fed_count(&self) -> usize {
self.fed_count
}
pub fn is_fed(&self, index: usize) -> bool {
self.fed.get(index).copied().unwrap_or(false)
}
pub fn truncate_rows(&mut self, n: usize) {
self.rows.truncate(n);
self.exponents.truncate(n);
}
pub fn feed(&mut self, index: usize, data: &[u8]) -> Result<(), RepairError> {
self.check_feed(index, data)?;
let c = self.constants[index];
self.rows
.par_iter_mut()
.zip(self.exponents.par_iter())
.for_each(|(row, &exp)| mul_add_prefix(row, data, gf::pow(c, exp)));
self.fed[index] = true;
self.fed_count += 1;
Ok(())
}
pub fn feed_batch(&mut self, slices: &[(usize, &[u8])]) -> Result<(), RepairError> {
for (k, &(index, data)) in slices.iter().enumerate() {
if let Err(e) = self.check_feed(index, data) {
for &(earlier, _) in &slices[..k] {
self.fed[earlier] = false;
}
return Err(e);
}
self.fed[index] = true;
}
let slice_size = self.slice_size;
let (full, short): (Vec<_>, Vec<_>) = slices
.iter()
.copied()
.partition(|(_, data)| data.len() == slice_size);
let full_data: Vec<&[u8]> = full.iter().map(|&(_, d)| d).collect();
let constants = &self.constants;
self.rows
.par_iter_mut()
.zip(self.exponents.par_iter())
.for_each(|(row, &exp)| {
let coeffs: Vec<u16> = full
.iter()
.map(|&(i, _)| gf::pow(constants[i], exp))
.collect();
gf_simd::mul_add_multi(row, &full_data, &coeffs);
for &(i, data) in &short {
mul_add_prefix(row, data, gf::pow(constants[i], exp));
}
});
self.fed_count += slices.len();
Ok(())
}
pub fn solve(&self, missing: &[usize]) -> Result<HashMap<usize, Vec<u8>>, RepairError> {
let mut missing = missing.to_vec();
missing.sort_unstable();
missing.dedup();
let outputs = self.reconstruct(&missing)?;
Ok(missing.into_iter().zip(outputs).collect())
}
fn check_feed(&self, index: usize, data: &[u8]) -> Result<(), RepairError> {
let total = self.fed.len();
if index >= total {
return Err(RepairError::SliceOutOfRange { index, total });
}
if data.len() > self.slice_size {
return Err(RepairError::SliceTooLong {
index,
len: data.len(),
slice_size: self.slice_size,
});
}
if self.fed[index] {
return Err(RepairError::DuplicateSlice { index });
}
Ok(())
}
fn reconstruct(&self, missing: &[usize]) -> Result<Vec<Vec<u8>>, RepairError> {
let total = self.fed.len();
for &index in missing {
if index >= total {
return Err(RepairError::SliceOutOfRange { index, total });
}
if self.fed[index] {
return Err(RepairError::DuplicateSlice { index });
}
}
let unaccounted = total - self.fed_count - missing.len();
if unaccounted != 0 {
return Err(RepairError::IncompleteFeed { unaccounted });
}
let d = missing.len();
if d == 0 {
return Ok(Vec::new());
}
if d > self.rows.len() {
return Err(RepairError::InsufficientRecovery {
needed: d as u32,
available: self.rows.len() as u32,
});
}
let mut vandermonde = GfMatrix::zeros(d, d);
for (e, &exp) in self.exponents[..d].iter().enumerate() {
for (j, &idx) in missing.iter().enumerate() {
vandermonde.set(e, j, gf::pow(self.constants[idx], exp));
}
}
let inverse = vandermonde.invert().ok_or(RepairError::SingularMatrix)?;
debug!(d, "D×D decode matrix inverted");
let row_refs: Vec<&[u8]> = self.rows[..d].iter().map(Vec::as_slice).collect();
let mut outputs: Vec<Vec<u8>> = (0..d).map(|_| vec![0u8; self.slice_size]).collect();
outputs.par_iter_mut().enumerate().for_each(|(j, dst)| {
let coeffs: Vec<u16> = (0..d).map(|e| inverse.get(j, e)).collect();
gf_simd::mul_add_multi(dst, &row_refs, &coeffs);
});
Ok(outputs)
}
}
fn mul_add_prefix(dst: &mut [u8], src: &[u8], c: u16) {
let even = src.len() & !1;
gf_simd::mul_add_buffer(&mut dst[..even], &src[..even], c);
if even < src.len() {
let p = gf::mul(c, src[even] as u16);
dst[even] ^= p as u8;
dst[even + 1] ^= (p >> 8) as u8;
}
}
pub fn repair(file_set: &Par2FileSet, dir: &Path) -> Result<RepairResult, RepairError> {
let verify_result = verify::verify(file_set, dir);
repair_from_verify_inner(file_set, dir, &verify_result, true)
}
pub fn repair_from_verify(
file_set: &Par2FileSet,
dir: &Path,
verify_result: &VerifyResult,
) -> Result<RepairResult, RepairError> {
repair_from_verify_inner(file_set, dir, verify_result, true)
}
pub fn repair_from_verify_no_reverify(
file_set: &Par2FileSet,
dir: &Path,
verify_result: &VerifyResult,
) -> Result<RepairResult, RepairError> {
repair_from_verify_inner(file_set, dir, verify_result, false)
}
fn repair_from_verify_inner(
file_set: &Par2FileSet,
dir: &Path,
verify_result: &VerifyResult,
re_verify: bool,
) -> Result<RepairResult, RepairError> {
if verify_result.all_correct() {
return Err(RepairError::NoDamage);
}
let blocks_needed = verify_result.blocks_needed();
info!(
blocks_needed,
damaged = verify_result.damaged.len(),
missing = verify_result.missing.len(),
"Repair: damage detected"
);
let recovery_blocks = load_recovery_blocks(dir, &file_set.recovery_set_id, file_set.slice_size);
let block_map = build_block_map(file_set);
let total_input_blocks = block_map.total_blocks as usize;
let slice_size = file_set.slice_size as usize;
let mut fold = Repair::new_owning(recovery_blocks, slice_size, total_input_blocks)?;
if (fold.rows() as u32) < blocks_needed {
return Err(RepairError::InsufficientRecovery {
needed: blocks_needed,
available: fold.rows() as u32,
});
}
let damaged_indices = find_damaged_block_indices(verify_result, &block_map);
info!(
damaged_block_count = damaged_indices.len(),
total_input_blocks, "Mapped damaged blocks to global indices"
);
fold.truncate_rows(damaged_indices.len());
let damaged_set: HashSet<usize> = damaged_indices.iter().copied().collect();
let intact_indices: Vec<usize> = (0..total_input_blocks)
.filter(|i| !damaged_set.contains(i))
.collect();
const BATCH_SIZE: usize = 24;
let mut file_handles: HashMap<String, std::fs::File> = HashMap::new();
for batch in intact_indices.chunks(BATCH_SIZE) {
let batch_data: Vec<Vec<u8>> = batch
.iter()
.map(|&idx| read_source_block(dir, &block_map, idx, slice_size, &mut file_handles))
.collect::<std::io::Result<Vec<_>>>()?;
let pairs: Vec<(usize, &[u8])> = batch
.iter()
.copied()
.zip(batch_data.iter().map(Vec::as_slice))
.collect();
fold.feed_batch(&pairs)?;
}
info!("Intact-block contributions subtracted from recovery data");
let mut repaired = fold.solve(&damaged_indices)?;
drop(fold);
info!("Repaired blocks reconstructed via D×D inverse");
let mut files_touched = HashSet::new();
for global_idx in &damaged_indices {
let data = repaired
.remove(global_idx)
.expect("solve returns every missing index");
let loc = block_map
.global_to_file(*global_idx)
.expect("damaged index comes from the block map");
let filename = &loc.file.filename;
let file_path = dir.join(filename);
debug!(
filename,
global_block = global_idx,
offset = loc.offset,
len = loc.len,
"Writing repaired block"
);
let mut f = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&file_path)?;
if f.metadata()?.len() < loc.file.file_size {
f.set_len(loc.file.file_size)?;
}
f.seek(SeekFrom::Start(loc.offset))?;
f.write_all(&data[..loc.len])?;
files_touched.insert(filename.clone());
}
let blocks_repaired = damaged_indices.len() as u32;
if re_verify {
let verification = verify::verify(file_set, dir);
if verification.all_correct() {
info!(
blocks = blocks_repaired,
files = files_touched.len(),
"Repair successful — all files verified"
);
Ok(RepairResult {
success: true,
blocks_repaired,
files_repaired: files_touched.len(),
message: "All files repaired and verified".to_string(),
})
} else {
Err(RepairError::VerifyFailed(format!("{verification}")))
}
} else {
info!(
blocks = blocks_repaired,
files = files_touched.len(),
"Repair complete (re-verify skipped)"
);
Ok(RepairResult {
success: true,
blocks_repaired,
files_repaired: files_touched.len(),
message: "All files repaired (re-verify skipped)".to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct BlockMap {
pub files: Vec<BlockFile>,
pub total_blocks: u32,
pub slice_size: u64,
}
#[derive(Debug, Clone)]
pub struct BlockFile {
pub file_id: Id16,
pub filename: String,
pub file_size: u64,
pub block_count: u32,
pub start_block: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct SliceLocation<'a> {
pub file: &'a BlockFile,
pub local_index: u32,
pub offset: u64,
pub len: usize,
}
pub fn build_block_map(file_set: &Par2FileSet) -> BlockMap {
let slice_size = file_set.slice_size;
let mut files = Vec::new();
let mut block_offset = 0u32;
let ordered_files: Vec<_> = if file_set.file_order.is_empty() {
let mut fallback: Vec<_> = file_set.files.values().collect();
fallback.sort_by_key(|f| f.file_id);
fallback
} else {
file_set
.file_order
.iter()
.filter_map(|id| file_set.files.get(id))
.collect()
};
for f in ordered_files {
let block_count = if slice_size == 0 {
0
} else {
f.size.div_ceil(slice_size) as u32
};
files.push(BlockFile {
file_id: f.file_id,
filename: f.filename.clone(),
file_size: f.size,
block_count,
start_block: block_offset,
});
block_offset += block_count;
}
BlockMap {
files,
total_blocks: block_offset,
slice_size,
}
}
impl BlockMap {
pub fn new(file_set: &Par2FileSet) -> Self {
build_block_map(file_set)
}
pub fn global_to_file(&self, global_idx: usize) -> Option<SliceLocation<'_>> {
let global = u32::try_from(global_idx).ok()?;
let pos = self
.files
.partition_point(|f| f.start_block + f.block_count <= global);
let file = self.files.get(pos)?;
if global < file.start_block {
return None;
}
let local_index = global - file.start_block;
let offset = local_index as u64 * self.slice_size;
let len = (file.file_size - offset).min(self.slice_size) as usize;
Some(SliceLocation {
file,
local_index,
offset,
len,
})
}
pub fn global_index(&self, filename: &str, local_index: u32) -> Option<usize> {
let f = self.file(filename)?;
(local_index < f.block_count).then(|| (f.start_block + local_index) as usize)
}
pub fn file(&self, filename: &str) -> Option<&BlockFile> {
self.files.iter().find(|f| f.filename == filename)
}
}
fn find_damaged_block_indices(verify_result: &VerifyResult, block_map: &BlockMap) -> Vec<usize> {
let mut indices = Vec::new();
for damaged in &verify_result.damaged {
if let Some(bf) = block_map.file(&damaged.filename) {
if damaged.damaged_block_indices.is_empty() {
for i in 0..bf.block_count {
indices.push((bf.start_block + i) as usize);
}
} else {
for &local_idx in &damaged.damaged_block_indices {
indices.push((bf.start_block + local_idx) as usize);
}
}
}
}
for missing in &verify_result.missing {
if let Some(bf) = block_map.file(&missing.filename) {
for i in 0..bf.block_count {
indices.push((bf.start_block + i) as usize);
}
}
}
indices.sort();
indices.dedup();
indices
}
fn read_source_block(
dir: &Path,
block_map: &BlockMap,
global_idx: usize,
slice_size: usize,
file_handles: &mut HashMap<String, std::fs::File>,
) -> std::io::Result<Vec<u8>> {
let loc = block_map
.global_to_file(global_idx)
.expect("intact index comes from the block map");
let handle = match file_handles.entry(loc.file.filename.clone()) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
let path = dir.join(&loc.file.filename);
e.insert(std::fs::File::open(&path)?)
}
};
handle.seek(SeekFrom::Start(loc.offset))?;
let mut buf = vec![0u8; slice_size]; let mut total = 0;
while total < slice_size {
match handle.read(&mut buf[total..]) {
Ok(0) => break,
Ok(n) => total += n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use crc32fast::hash as crc32_hash;
use md5::{Digest, Md5};
use crate::packets::{HEADER_SIZE, MAGIC};
const TYPE_MAIN: &[u8; 16] = b"PAR 2.0\x00Main\x00\x00\x00\x00";
const TYPE_FILE_DESC: &[u8; 16] = b"PAR 2.0\x00FileDesc";
const TYPE_IFSC: &[u8; 16] = b"PAR 2.0\x00IFSC\x00\x00\x00\x00";
const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
fn build_packet(set_id: [u8; 16], packet_type: &[u8; 16], body: &[u8]) -> Vec<u8> {
let mut data = Vec::with_capacity(32 + body.len());
data.extend_from_slice(&set_id);
data.extend_from_slice(packet_type);
data.extend_from_slice(body);
let mut packet = Vec::with_capacity(32 + data.len());
packet.extend_from_slice(MAGIC);
packet.extend_from_slice(&((HEADER_SIZE + body.len()) as u64).to_le_bytes());
packet.extend_from_slice(&Md5::digest(&data));
packet.extend_from_slice(&data);
packet
}
fn file_md5(data: &[u8]) -> [u8; 16] {
Md5::digest(data).into()
}
fn hash_16k(data: &[u8]) -> [u8; 16] {
Md5::digest(&data[..data.len().min(16_384)]).into()
}
fn padded_block(data: &[u8], block_index: usize, slice_size: usize) -> Vec<u8> {
let start = block_index * slice_size;
let end = (start + slice_size).min(data.len());
let mut block = vec![0u8; slice_size];
if start < data.len() {
block[..end - start].copy_from_slice(&data[start..end]);
}
block
}
fn write_par2_fixture(
dir: &Path,
slice_size: usize,
files: &[(&str, [u8; 16], Vec<u8>)],
main_order: &[[u8; 16]],
recovery_exponents: &[u32],
) -> PathBuf {
let set_id = [0x5Au8; 16];
let mut index_bytes = Vec::new();
let mut main_body = Vec::new();
main_body.extend_from_slice(&(slice_size as u64).to_le_bytes());
main_body.extend_from_slice(&(main_order.len() as u32).to_le_bytes());
for file_id in main_order {
main_body.extend_from_slice(file_id);
}
index_bytes.extend_from_slice(&build_packet(set_id, TYPE_MAIN, &main_body));
for (filename, file_id, data) in files {
std::fs::write(dir.join(filename), data).unwrap();
let mut desc_body = Vec::new();
desc_body.extend_from_slice(file_id);
desc_body.extend_from_slice(&file_md5(data));
desc_body.extend_from_slice(&hash_16k(data));
desc_body.extend_from_slice(&(data.len() as u64).to_le_bytes());
desc_body.extend_from_slice(filename.as_bytes());
desc_body.push(0);
while desc_body.len() % 4 != 0 {
desc_body.push(0);
}
index_bytes.extend_from_slice(&build_packet(set_id, TYPE_FILE_DESC, &desc_body));
let block_count = data.len().div_ceil(slice_size);
let mut ifsc_body = Vec::new();
ifsc_body.extend_from_slice(file_id);
for block_idx in 0..block_count {
let block = padded_block(data, block_idx, slice_size);
ifsc_body.extend_from_slice(&file_md5(&block));
ifsc_body.extend_from_slice(&crc32_hash(&block).to_le_bytes());
}
index_bytes.extend_from_slice(&build_packet(set_id, TYPE_IFSC, &ifsc_body));
}
std::fs::write(dir.join("fixture.par2"), &index_bytes).unwrap();
let ordered_files: Vec<_> = main_order
.iter()
.map(|wanted| {
files
.iter()
.find(|(_, file_id, _)| file_id == wanted)
.unwrap()
})
.collect();
let input_blocks: Vec<Vec<u8>> = ordered_files
.iter()
.flat_map(|(_, _, data)| {
let count = data.len().div_ceil(slice_size);
(0..count).map(move |block_idx| padded_block(data, block_idx, slice_size))
})
.collect();
let input_count = input_blocks.len();
let enc = GfMatrix::par2_encoding_matrix(input_count, recovery_exponents);
let srcs: Vec<&[u8]> = input_blocks.iter().map(Vec::as_slice).collect();
let mut vol_bytes = index_bytes.clone();
for (row_idx, &exp) in recovery_exponents.iter().enumerate() {
let coeffs: Vec<u16> = (0..input_count)
.map(|col| enc.get(input_count + row_idx, col))
.collect();
let mut recovery = vec![0u8; slice_size];
gf_simd::mul_add_multi(&mut recovery, &srcs, &coeffs);
let mut body = Vec::with_capacity(4 + slice_size);
body.extend_from_slice(&exp.to_le_bytes());
body.extend_from_slice(&recovery);
vol_bytes.extend_from_slice(&build_packet(set_id, TYPE_RECOVERY, &body));
}
std::fs::write(dir.join("fixture.vol00+2.par2"), &vol_bytes).unwrap();
dir.join("fixture.par2")
}
#[test]
fn test_rs_roundtrip_simple() {
let input0: Vec<u8> = vec![0x01, 0x00, 0x02, 0x00]; let input1: Vec<u8> = vec![0x03, 0x00, 0x04, 0x00];
let input_count = 2;
let recovery_exponents = vec![0u32, 1u32];
let enc = GfMatrix::par2_encoding_matrix(input_count, &recovery_exponents);
let slice_size = 4;
let u16_per_slice = slice_size / 2;
let inputs = [&input0, &input1];
let mut recovery0 = vec![0u8; slice_size];
let mut recovery1 = vec![0u8; slice_size];
for pos in 0..u16_per_slice {
let off = pos * 2;
let mut r0: u16 = 0;
let mut r1: u16 = 0;
for (i, inp) in inputs.iter().enumerate() {
let val = u16::from_le_bytes([inp[off], inp[off + 1]]);
r0 = gf::add(r0, gf::mul(enc.get(2, i), val));
r1 = gf::add(r1, gf::mul(enc.get(3, i), val));
}
recovery0[off] = r0 as u8;
recovery0[off + 1] = (r0 >> 8) as u8;
recovery1[off] = r1 as u8;
recovery1[off + 1] = (r1 >> 8) as u8;
}
let constants = par2_input_constants(input_count);
let damaged_indices = [0usize, 1usize];
let num_damaged = damaged_indices.len();
let mut vandermonde = GfMatrix::zeros(num_damaged, num_damaged);
for (e, &exp) in recovery_exponents.iter().enumerate() {
for (j, &dmg_idx) in damaged_indices.iter().enumerate() {
vandermonde.set(e, j, gf::pow(constants[dmg_idx], exp));
}
}
let inv = vandermonde.invert().expect("Should be invertible");
let adjusted = [&recovery0[..], &recovery1[..]];
let mut result0 = vec![0u8; slice_size];
let mut result1 = vec![0u8; slice_size];
for pos in 0..u16_per_slice {
let off = pos * 2;
let mut out0: u16 = 0;
let mut out1: u16 = 0;
for (e, adj) in adjusted.iter().enumerate() {
let val = u16::from_le_bytes([adj[off], adj[off + 1]]);
out0 = gf::add(out0, gf::mul(inv.get(0, e), val));
out1 = gf::add(out1, gf::mul(inv.get(1, e), val));
}
result0[off] = out0 as u8;
result0[off + 1] = (out0 >> 8) as u8;
result1[off] = out1 as u8;
result1[off + 1] = (out1 >> 8) as u8;
}
assert_eq!(result0, input0, "Recovered block 0 should match original");
assert_eq!(result1, input1, "Recovered block 1 should match original");
}
#[test]
fn test_rs_roundtrip_partial_damage() {
let slice_size = 4;
let input_count = 4;
let recovery_exponents = vec![0u32, 1u32];
let inputs: Vec<Vec<u8>> = vec![
vec![0x01, 0x00, 0x02, 0x00],
vec![0x03, 0x00, 0x04, 0x00],
vec![0x05, 0x00, 0x06, 0x00],
vec![0x07, 0x00, 0x08, 0x00],
];
let enc = GfMatrix::par2_encoding_matrix(input_count, &recovery_exponents);
let mut recovery = vec![vec![0u8; slice_size]; 2];
for pos in 0..(slice_size / 2) {
let off = pos * 2;
for (e, rec) in recovery.iter_mut().enumerate() {
let mut val: u16 = 0;
for (i, inp) in inputs.iter().enumerate() {
let d = u16::from_le_bytes([inp[off], inp[off + 1]]);
val = gf::add(val, gf::mul(enc.get(input_count + e, i), d));
}
rec[off] = val as u8;
rec[off + 1] = (val >> 8) as u8;
}
}
let damaged_indices = [1usize, 3usize];
let intact_indices: Vec<usize> = (0..input_count)
.filter(|i| !damaged_indices.contains(i))
.collect();
let num_damaged = damaged_indices.len();
let constants = par2_input_constants(input_count);
let mut vandermonde = GfMatrix::zeros(num_damaged, num_damaged);
for (e, &exp) in recovery_exponents.iter().enumerate() {
for (j, &dmg_idx) in damaged_indices.iter().enumerate() {
vandermonde.set(e, j, gf::pow(constants[dmg_idx], exp));
}
}
let inv = vandermonde.invert().expect("Should be invertible");
let mut adjusted = recovery.clone();
for &intact_idx in &intact_indices {
let c_i = constants[intact_idx];
for (e, adj) in adjusted.iter_mut().enumerate() {
let coeff = gf::pow(c_i, recovery_exponents[e]);
gf_simd::mul_add_buffer(adj, &inputs[intact_idx], coeff);
}
}
let adj_refs: Vec<&[u8]> = adjusted.iter().map(|v| v.as_slice()).collect();
let mut outputs: Vec<Vec<u8>> = (0..num_damaged).map(|_| vec![0u8; slice_size]).collect();
for (j, dst) in outputs.iter_mut().enumerate() {
let coeffs: Vec<u16> = (0..num_damaged).map(|e| inv.get(j, e)).collect();
gf_simd::mul_add_multi(dst, &adj_refs, &coeffs);
}
assert_eq!(outputs[0], inputs[1], "Recovered block 1 should match");
assert_eq!(outputs[1], inputs[3], "Recovered block 3 should match");
}
#[test]
fn test_repair_cross_file_damage_respects_main_packet_file_order() {
let dir = tempfile::tempdir().unwrap();
let slice_size = 4096usize;
let file_a_id = [0x20; 16];
let file_b_id = [0x10; 16];
let file_a = vec![0x41; slice_size * 2];
let file_b = vec![0x42; slice_size * 2];
let par2_path = write_par2_fixture(
dir.path(),
slice_size,
&[
("file_a.bin", file_a_id, file_a.clone()),
("file_b.bin", file_b_id, file_b.clone()),
],
&[file_a_id, file_b_id],
&[0, 1],
);
let file_set = crate::parse(&par2_path).unwrap();
assert_eq!(file_set.file_order, vec![file_a_id, file_b_id]);
{
let file_a_path = dir.path().join("file_a.bin");
let file_b_path = dir.path().join("file_b.bin");
let mut a = std::fs::OpenOptions::new()
.write(true)
.open(&file_a_path)
.unwrap();
let mut b = std::fs::OpenOptions::new()
.write(true)
.open(&file_b_path)
.unwrap();
a.seek(SeekFrom::Start(slice_size as u64)).unwrap();
b.seek(SeekFrom::Start(slice_size as u64)).unwrap();
a.write_all(&vec![0xDE; slice_size]).unwrap();
b.write_all(&vec![0xAD; slice_size]).unwrap();
}
let pre = verify::verify(&file_set, dir.path());
assert_eq!(pre.damaged.len(), 2);
assert_eq!(pre.blocks_needed(), 2);
assert!(pre.repair_possible);
let repaired = repair(&file_set, dir.path()).unwrap();
assert!(repaired.success);
assert_eq!(
std::fs::read(dir.path().join("file_a.bin")).unwrap(),
file_a
);
assert_eq!(
std::fs::read(dir.path().join("file_b.bin")).unwrap(),
file_b
);
}
}