use std::path::Path;
use tracing::{info, warn};
#[derive(Debug)]
pub struct Par2Result {
pub success: bool,
pub blocks_needed: u32,
pub blocks_available: u32,
pub repaired: bool,
pub message: String,
}
pub fn recovery_can_cover(blocks_needed: u32, blocks_available: u32) -> bool {
blocks_needed <= blocks_available
}
pub fn par2_repair_blocking(par2_file: &Path) -> anyhow::Result<Par2Result> {
let dir = par2_file.parent().unwrap_or(Path::new("."));
let file_set = rust_par2::parse(par2_file).map_err(|e| anyhow::anyhow!("PAR2 parse: {e}"))?;
let verify_result = rust_par2::verify(&file_set, dir);
if verify_result.all_correct() {
return Ok(Par2Result {
success: true,
blocks_needed: 0,
blocks_available: verify_result.recovery_blocks_available,
repaired: false,
message: "All files correct".to_string(),
});
}
let blocks_needed = verify_result.blocks_needed();
let blocks_available = verify_result.recovery_blocks_available;
info!(
blocks_needed,
blocks_available,
damaged = verify_result.damaged.len(),
missing = verify_result.missing.len(),
"Damage detected, attempting native repair"
);
if !recovery_can_cover(blocks_needed, blocks_available) {
let message = format!(
"Insufficient recovery data: need {blocks_needed} blocks, have {blocks_available}"
);
warn!(
blocks_needed,
blocks_available, "Native PAR2 repair cannot cover damage"
);
return Ok(Par2Result {
success: false,
blocks_needed,
blocks_available,
repaired: false,
message,
});
}
match rust_par2::repair_from_verify(&file_set, dir, &verify_result) {
Ok(repair_result) => {
info!(
blocks_repaired = repair_result.blocks_repaired,
files_repaired = repair_result.files_repaired,
"Native PAR2 repair complete"
);
Ok(Par2Result {
success: repair_result.success,
blocks_needed,
blocks_available,
repaired: repair_result.success,
message: repair_result.message,
})
}
Err(e) => {
warn!(error = %e, "Native PAR2 repair failed");
Ok(Par2Result {
success: false,
blocks_needed,
blocks_available,
repaired: false,
message: format!("{e}"),
})
}
}
}
pub async fn par2_repair(par2_file: &Path) -> anyhow::Result<Par2Result> {
let par2_file = par2_file.to_path_buf();
tokio::task::spawn_blocking(move || par2_repair_blocking(&par2_file)).await?
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_repair_nonexistent_file() {
let result = par2_repair(Path::new("/nonexistent/file.par2")).await;
assert!(result.is_err() || !result.unwrap().success);
}
#[test]
fn test_repair_blocking_nonexistent() {
let result = par2_repair_blocking(Path::new("/no/such/file.par2"));
assert!(result.is_err());
}
#[test]
fn test_par2_result_fields() {
let result = Par2Result {
success: true,
blocks_needed: 0,
blocks_available: 10,
repaired: false,
message: "All files correct".to_string(),
};
assert!(result.success);
assert!(!result.repaired);
assert_eq!(result.blocks_needed, 0);
assert_eq!(result.blocks_available, 10);
}
#[test]
fn repair_succeeds_when_recovery_covers_damage() {
assert!(recovery_can_cover(10, 10));
assert!(recovery_can_cover(2, 671));
}
#[test]
fn repair_fails_when_recovery_is_insufficient() {
assert!(!recovery_can_cover(207, 71));
assert!(!recovery_can_cover(1, 0));
}
}