1use std::io::{Read, Seek, SeekFrom};
7use std::path::Path;
8
9use tracing::{debug, info, warn};
10
11use crate::packets::{HEADER_SIZE, MAGIC};
12
13const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
15
16#[derive(Debug)]
18pub struct RecoveryBlock {
19 pub exponent: u32,
21 pub data: Vec<u8>,
23}
24
25pub fn load_recovery_blocks(dir: &Path, set_id: &[u8; 16], slice_size: u64) -> Vec<RecoveryBlock> {
30 let mut blocks = Vec::new();
31
32 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
76fn 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 if file.read_exact(&mut header_buf).is_err() {
94 break;
95 }
96
97 if &header_buf[0..8] != MAGIC {
99 pos += 4; continue;
101 }
102
103 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 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 let pkt_type = &header_buf[48..64];
119 if pkt_type == TYPE_RECOVERY {
120 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}