1use std::path::Path;
7
8use tracing::{info, warn};
9
10#[derive(Debug)]
12pub struct Par2Result {
13 pub success: bool,
14 pub blocks_needed: u32,
15 pub blocks_available: u32,
16 pub repaired: bool,
17 pub message: String,
18}
19
20pub fn recovery_can_cover(blocks_needed: u32, blocks_available: u32) -> bool {
22 blocks_needed <= blocks_available
23}
24
25pub fn par2_repair_blocking(par2_file: &Path) -> anyhow::Result<Par2Result> {
30 let dir = par2_file.parent().unwrap_or(Path::new("."));
31
32 let file_set = rust_par2::parse(par2_file).map_err(|e| anyhow::anyhow!("PAR2 parse: {e}"))?;
33 let verify_result = rust_par2::verify(&file_set, dir);
34
35 if verify_result.all_correct() {
36 return Ok(Par2Result {
37 success: true,
38 blocks_needed: 0,
39 blocks_available: verify_result.recovery_blocks_available,
40 repaired: false,
41 message: "All files correct".to_string(),
42 });
43 }
44
45 let blocks_needed = verify_result.blocks_needed();
46 let blocks_available = verify_result.recovery_blocks_available;
47
48 info!(
49 blocks_needed,
50 blocks_available,
51 damaged = verify_result.damaged.len(),
52 missing = verify_result.missing.len(),
53 "Damage detected, attempting native repair"
54 );
55
56 if !recovery_can_cover(blocks_needed, blocks_available) {
57 let message = format!(
58 "Insufficient recovery data: need {blocks_needed} blocks, have {blocks_available}"
59 );
60 warn!(
61 blocks_needed,
62 blocks_available, "Native PAR2 repair cannot cover damage"
63 );
64 return Ok(Par2Result {
65 success: false,
66 blocks_needed,
67 blocks_available,
68 repaired: false,
69 message,
70 });
71 }
72
73 match rust_par2::repair_from_verify(&file_set, dir, &verify_result) {
74 Ok(repair_result) => {
75 info!(
76 blocks_repaired = repair_result.blocks_repaired,
77 files_repaired = repair_result.files_repaired,
78 "Native PAR2 repair complete"
79 );
80 Ok(Par2Result {
81 success: repair_result.success,
82 blocks_needed,
83 blocks_available,
84 repaired: repair_result.success,
85 message: repair_result.message,
86 })
87 }
88 Err(e) => {
89 warn!(error = %e, "Native PAR2 repair failed");
90 Ok(Par2Result {
91 success: false,
92 blocks_needed,
93 blocks_available,
94 repaired: false,
95 message: format!("{e}"),
96 })
97 }
98 }
99}
100
101pub async fn par2_repair(par2_file: &Path) -> anyhow::Result<Par2Result> {
103 let par2_file = par2_file.to_path_buf();
104 tokio::task::spawn_blocking(move || par2_repair_blocking(&par2_file)).await?
105}
106
107#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[tokio::test]
116 async fn test_repair_nonexistent_file() {
117 let result = par2_repair(Path::new("/nonexistent/file.par2")).await;
118 assert!(result.is_err() || !result.unwrap().success);
119 }
120
121 #[test]
122 fn test_repair_blocking_nonexistent() {
123 let result = par2_repair_blocking(Path::new("/no/such/file.par2"));
124 assert!(result.is_err());
125 }
126
127 #[test]
128 fn test_par2_result_fields() {
129 let result = Par2Result {
130 success: true,
131 blocks_needed: 0,
132 blocks_available: 10,
133 repaired: false,
134 message: "All files correct".to_string(),
135 };
136 assert!(result.success);
137 assert!(!result.repaired);
138 assert_eq!(result.blocks_needed, 0);
139 assert_eq!(result.blocks_available, 10);
140 }
141
142 #[test]
143 fn repair_succeeds_when_recovery_covers_damage() {
144 assert!(recovery_can_cover(10, 10));
145 assert!(recovery_can_cover(2, 671));
146 }
147
148 #[test]
149 fn repair_fails_when_recovery_is_insufficient() {
150 assert!(!recovery_can_cover(207, 71));
151 assert!(!recovery_can_cover(1, 0));
152 }
153}