use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use tracing::{debug, info, warn};
use crate::packets::{HEADER_SIZE, MAGIC};
const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
#[derive(Debug)]
pub struct RecoveryBlock {
pub exponent: u32,
pub data: Vec<u8>,
}
pub fn load_recovery_blocks(dir: &Path, set_id: &[u8; 16], slice_size: u64) -> Vec<RecoveryBlock> {
let mut blocks = Vec::new();
let mut par2_files: Vec<_> = match std::fs::read_dir(dir) {
Ok(entries) => entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("par2"))
})
.collect(),
Err(e) => {
warn!(error = %e, "Failed to read directory for recovery files");
return blocks;
}
};
par2_files.sort();
for par2_path in &par2_files {
match read_recovery_packets(par2_path, set_id, slice_size) {
Ok(mut file_blocks) => {
debug!(
file = %par2_path.display(),
count = file_blocks.len(),
"Loaded recovery blocks"
);
blocks.append(&mut file_blocks);
}
Err(e) => {
debug!(
file = %par2_path.display(),
error = %e,
"Skipping file (no recovery blocks or read error)"
);
}
}
}
blocks.sort_by_key(|b| b.exponent);
info!(total_blocks = blocks.len(), "Recovery blocks loaded");
blocks
}
fn read_recovery_packets(
path: &Path,
set_id: &[u8; 16],
slice_size: u64,
) -> std::io::Result<Vec<RecoveryBlock>> {
let mut file = std::fs::File::open(path)?;
let file_size = file.metadata()?.len();
let mut blocks = Vec::new();
let mut pos: u64 = 0;
let mut header_buf = [0u8; HEADER_SIZE];
while pos + HEADER_SIZE as u64 <= file_size {
file.seek(SeekFrom::Start(pos))?;
if file.read_exact(&mut header_buf).is_err() {
break;
}
if &header_buf[0..8] != MAGIC {
pos += 4; continue;
}
let packet_len = u64::from_le_bytes(header_buf[8..16].try_into().unwrap());
if packet_len < HEADER_SIZE as u64 || packet_len % 4 != 0 {
pos += 4;
continue;
}
let pkt_set_id: [u8; 16] = header_buf[32..48].try_into().unwrap();
if &pkt_set_id != set_id {
pos += packet_len;
continue;
}
let pkt_type = &header_buf[48..64];
if pkt_type == TYPE_RECOVERY {
let body_len = packet_len - HEADER_SIZE as u64;
let expected_body = 4 + slice_size;
if body_len >= expected_body {
let mut body = vec![0u8; expected_body as usize];
file.seek(SeekFrom::Start(pos + HEADER_SIZE as u64))?;
file.read_exact(&mut body)?;
let exponent = u32::from_le_bytes(body[0..4].try_into().unwrap());
let data = body[4..4 + slice_size as usize].to_vec();
blocks.push(RecoveryBlock { exponent, data });
}
}
pos += packet_len;
}
Ok(blocks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_empty_dir() {
let dir = tempfile::tempdir().unwrap();
let set_id = [0u8; 16];
let blocks = load_recovery_blocks(dir.path(), &set_id, 768000);
assert!(blocks.is_empty());
}
}