Skip to main content

rust_par2/
recovery.rs

1//! Recovery block reading from PAR2 volume files.
2//!
3//! Scans all `.par2` files in a directory and extracts RecoverySlice packets,
4//! which contain the actual Reed-Solomon recovery data needed for repair.
5
6use std::io::{Read, Seek, SeekFrom};
7use std::path::Path;
8
9use tracing::{debug, info, warn};
10
11use crate::packets::{HEADER_SIZE, MAGIC};
12
13/// RecoverySlice packet type identifier.
14const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
15
16/// A recovery block extracted from a RecoverySlice packet.
17#[derive(Debug)]
18pub struct RecoveryBlock {
19    /// The exponent (recovery block index) used in the Vandermonde matrix.
20    pub exponent: u32,
21    /// The recovery data. Length = slice_size.
22    pub data: Vec<u8>,
23}
24
25/// Load all recovery blocks from PAR2 files in a directory.
26///
27/// Scans all `.par2` files (index and volumes) for RecoverySlice packets
28/// matching the given recovery set ID. Returns blocks sorted by exponent.
29pub fn load_recovery_blocks(dir: &Path, set_id: &[u8; 16], slice_size: u64) -> Vec<RecoveryBlock> {
30    let mut blocks = Vec::new();
31
32    // Find all .par2 files in the directory
33    let mut par2_files: Vec<_> = match std::fs::read_dir(dir) {
34        Ok(entries) => entries
35            .filter_map(|e| e.ok())
36            .map(|e| e.path())
37            .filter(|p| {
38                p.extension()
39                    .is_some_and(|ext| ext.eq_ignore_ascii_case("par2"))
40            })
41            .collect(),
42        Err(e) => {
43            warn!(error = %e, "Failed to read directory for recovery files");
44            return blocks;
45        }
46    };
47    par2_files.sort();
48
49    for par2_path in &par2_files {
50        match read_recovery_packets(par2_path, set_id, slice_size) {
51            Ok(mut file_blocks) => {
52                debug!(
53                    file = %par2_path.display(),
54                    count = file_blocks.len(),
55                    "Loaded recovery blocks"
56                );
57                blocks.append(&mut file_blocks);
58            }
59            Err(e) => {
60                debug!(
61                    file = %par2_path.display(),
62                    error = %e,
63                    "Skipping file (no recovery blocks or read error)"
64                );
65            }
66        }
67    }
68
69    blocks.sort_by_key(|b| b.exponent);
70
71    info!(total_blocks = blocks.len(), "Recovery blocks loaded");
72
73    blocks
74}
75
76/// Read RecoverySlice packets from a single PAR2 file.
77fn read_recovery_packets(
78    path: &Path,
79    set_id: &[u8; 16],
80    slice_size: u64,
81) -> std::io::Result<Vec<RecoveryBlock>> {
82    let mut file = std::fs::File::open(path)?;
83    let file_size = file.metadata()?.len();
84    let mut blocks = Vec::new();
85    let mut pos: u64 = 0;
86
87    let mut header_buf = [0u8; HEADER_SIZE];
88
89    while pos + HEADER_SIZE as u64 <= file_size {
90        file.seek(SeekFrom::Start(pos))?;
91
92        // Read packet header
93        if file.read_exact(&mut header_buf).is_err() {
94            break;
95        }
96
97        // Check magic
98        if &header_buf[0..8] != MAGIC {
99            pos += 4; // Scan forward
100            continue;
101        }
102
103        // Parse packet length
104        let packet_len = u64::from_le_bytes(header_buf[8..16].try_into().unwrap());
105        if packet_len < HEADER_SIZE as u64 || packet_len % 4 != 0 {
106            pos += 4;
107            continue;
108        }
109
110        // Check recovery set ID
111        let pkt_set_id: [u8; 16] = header_buf[32..48].try_into().unwrap();
112        if &pkt_set_id != set_id {
113            pos += packet_len;
114            continue;
115        }
116
117        // Check packet type
118        let pkt_type = &header_buf[48..64];
119        if pkt_type == TYPE_RECOVERY {
120            // RecoverySlice packet body layout:
121            // Offset 0..4 (within body): exponent (u32 LE)
122            // Offset 4..4+slice_size: recovery data
123            let body_len = packet_len - HEADER_SIZE as u64;
124            let expected_body = 4 + slice_size;
125
126            if body_len >= expected_body {
127                let mut body = vec![0u8; expected_body as usize];
128                file.seek(SeekFrom::Start(pos + HEADER_SIZE as u64))?;
129                file.read_exact(&mut body)?;
130
131                let exponent = u32::from_le_bytes(body[0..4].try_into().unwrap());
132                let data = body[4..4 + slice_size as usize].to_vec();
133
134                blocks.push(RecoveryBlock { exponent, data });
135            }
136        }
137
138        pos += packet_len;
139    }
140
141    Ok(blocks)
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_load_empty_dir() {
150        let dir = tempfile::tempdir().unwrap();
151        let set_id = [0u8; 16];
152        let blocks = load_recovery_blocks(dir.path(), &set_id, 768000);
153        assert!(blocks.is_empty());
154    }
155}