Skip to main content

nzb_postproc/
par2.rs

1//! Par2 verification and repair via pure-Rust `rust-par2` library.
2//!
3//! No external binary or subprocess is needed — all PAR2 parsing, verification,
4//! and Reed-Solomon repair runs natively in-process.
5
6use std::path::Path;
7
8use tracing::{info, warn};
9
10/// Result of a par2 verify/repair operation.
11#[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
20/// Repair damaged/missing files using native Rust PAR2 repair.
21///
22/// This runs verification internally and then repairs if needed.
23/// Blocking — call from `spawn_blocking` in async contexts.
24pub fn par2_repair_blocking(par2_file: &Path) -> anyhow::Result<Par2Result> {
25    let dir = par2_file.parent().unwrap_or(Path::new("."));
26
27    let file_set = rust_par2::parse(par2_file).map_err(|e| anyhow::anyhow!("PAR2 parse: {e}"))?;
28    let verify_result = rust_par2::verify(&file_set, dir);
29
30    if verify_result.all_correct() {
31        return Ok(Par2Result {
32            success: true,
33            blocks_needed: 0,
34            blocks_available: verify_result.recovery_blocks_available,
35            repaired: false,
36            message: "All files correct".to_string(),
37        });
38    }
39
40    let blocks_needed = verify_result.blocks_needed();
41    let blocks_available = verify_result.recovery_blocks_available;
42
43    info!(
44        blocks_needed,
45        blocks_available,
46        damaged = verify_result.damaged.len(),
47        missing = verify_result.missing.len(),
48        "Damage detected, attempting native repair"
49    );
50
51    match rust_par2::repair_from_verify(&file_set, dir, &verify_result) {
52        Ok(repair_result) => {
53            info!(
54                blocks_repaired = repair_result.blocks_repaired,
55                files_repaired = repair_result.files_repaired,
56                "Native PAR2 repair complete"
57            );
58            Ok(Par2Result {
59                success: repair_result.success,
60                blocks_needed,
61                blocks_available,
62                repaired: repair_result.success,
63                message: repair_result.message,
64            })
65        }
66        Err(e) => {
67            warn!(error = %e, "Native PAR2 repair failed");
68            Ok(Par2Result {
69                success: false,
70                blocks_needed,
71                blocks_available,
72                repaired: false,
73                message: format!("{e}"),
74            })
75        }
76    }
77}
78
79/// Async wrapper around `par2_repair_blocking`.
80pub async fn par2_repair(par2_file: &Path) -> anyhow::Result<Par2Result> {
81    let par2_file = par2_file.to_path_buf();
82    tokio::task::spawn_blocking(move || par2_repair_blocking(&par2_file)).await?
83}
84
85// ---------------------------------------------------------------------------
86// Tests
87// ---------------------------------------------------------------------------
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[tokio::test]
94    async fn test_repair_nonexistent_file() {
95        let result = par2_repair(Path::new("/nonexistent/file.par2")).await;
96        assert!(result.is_err() || !result.unwrap().success);
97    }
98
99    #[test]
100    fn test_repair_blocking_nonexistent() {
101        let result = par2_repair_blocking(Path::new("/no/such/file.par2"));
102        assert!(result.is_err());
103    }
104
105    #[test]
106    fn test_par2_result_fields() {
107        let result = Par2Result {
108            success: true,
109            blocks_needed: 0,
110            blocks_available: 10,
111            repaired: false,
112            message: "All files correct".to_string(),
113        };
114        assert!(result.success);
115        assert!(!result.repaired);
116        assert_eq!(result.blocks_needed, 0);
117        assert_eq!(result.blocks_available, 10);
118    }
119}