use std::collections::BTreeSet;
use std::fmt;
use std::io::{self, Read};
use std::path::Path;
use md5::{Digest, Md5};
use tracing::{debug, info, warn};
use crate::packets::{HEADER_SIZE, MAGIC};
const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
pub const DEFAULT_MAX_VOLUME_BYTES: u64 = 512 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct RecoveryBlock {
pub exponent: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RecoveryBudget {
pub max_rows: Option<usize>,
pub max_bytes: Option<u64>,
}
impl RecoveryBudget {
pub const UNLIMITED: Self = Self {
max_rows: None,
max_bytes: None,
};
pub fn rows(n: usize) -> Self {
Self {
max_rows: Some(n),
max_bytes: None,
}
}
pub fn bytes(bytes: u64) -> Self {
Self {
max_rows: None,
max_bytes: Some(bytes),
}
}
pub fn row_limit(&self, slice_size: u64) -> usize {
let by_bytes = match self.max_bytes {
Some(b) if slice_size > 0 => usize::try_from(b / slice_size).unwrap_or(usize::MAX),
_ => usize::MAX,
};
self.max_rows.unwrap_or(usize::MAX).min(by_bytes)
}
}
#[derive(Debug)]
pub enum VolumeEnd {
Complete,
Truncated,
CapReached,
Io(io::Error),
}
impl fmt::Display for VolumeEnd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Complete => write!(f, "read to end"),
Self::Truncated => write!(f, "truncated inside a packet"),
Self::CapReached => write!(f, "stopped at the per-volume byte cap"),
Self::Io(e) => write!(f, "read error: {e}"),
}
}
}
#[derive(Debug)]
pub struct VolumeScan {
pub seen: usize,
pub kept: usize,
pub corrupt: usize,
pub bytes_read: u64,
pub end: VolumeEnd,
}
impl VolumeScan {
pub fn read_to_end(&self) -> bool {
matches!(self.end, VolumeEnd::Complete)
}
}
#[derive(Debug, Clone)]
pub struct RecoveryReader {
set_id: [u8; 16],
slice_size: u64,
limit: usize,
max_volume_bytes: u64,
blocks: Vec<RecoveryBlock>,
exponents: BTreeSet<u32>,
seen: usize,
}
impl RecoveryReader {
pub fn new(set_id: [u8; 16], slice_size: u64) -> Self {
Self {
set_id,
slice_size,
limit: usize::MAX,
max_volume_bytes: DEFAULT_MAX_VOLUME_BYTES,
blocks: Vec::new(),
exponents: BTreeSet::new(),
seen: 0,
}
}
pub fn with_budget(mut self, budget: RecoveryBudget) -> Self {
self.set_budget(budget);
self
}
pub fn with_max_volume_bytes(mut self, max: u64) -> Self {
self.max_volume_bytes = max;
self
}
pub fn set_budget(&mut self, budget: RecoveryBudget) {
self.limit = budget.row_limit(self.slice_size);
while self.blocks.len() > self.limit {
self.evict_highest();
}
}
pub fn row_limit(&self) -> usize {
self.limit
}
pub fn seen(&self) -> usize {
self.seen
}
pub fn kept(&self) -> usize {
self.blocks.len()
}
pub fn blocks(&self) -> &[RecoveryBlock] {
&self.blocks
}
pub fn into_blocks(self) -> Vec<RecoveryBlock> {
self.blocks
}
pub fn read_volume<R: Read>(&mut self, reader: R) -> VolumeScan {
let mut src = Source::new(reader, self.max_volume_bytes);
let mut scan = VolumeScan {
seen: 0,
kept: 0,
corrupt: 0,
bytes_read: 0,
end: VolumeEnd::Complete,
};
scan.end = match self.scan(&mut src, &mut scan) {
Ok(()) => VolumeEnd::Complete,
Err(Stop::Eof) => VolumeEnd::Truncated,
Err(Stop::Cap) => VolumeEnd::CapReached,
Err(Stop::Io(e)) => VolumeEnd::Io(e),
};
scan.bytes_read = src.pos;
self.seen += scan.seen;
scan
}
fn scan<R: Read>(&mut self, src: &mut Source<R>, scan: &mut VolumeScan) -> Result<(), Stop> {
let mut hdr = [0u8; HEADER_SIZE];
let mut have = 0;
loop {
let got = src.read_full(&mut hdr[have..])?;
have += got;
if have < HEADER_SIZE {
return if have > 0 && starts_like_magic(&hdr[..have]) {
Err(Stop::Eof)
} else {
Ok(())
};
}
if &hdr[0..8] != MAGIC {
let next = find_magic_start(&hdr[1..]).map_or(HEADER_SIZE, |i| i + 1);
hdr.copy_within(next.., 0);
have = HEADER_SIZE - next;
continue;
}
let packet_len = u64::from_le_bytes(hdr[8..16].try_into().unwrap());
if packet_len < HEADER_SIZE as u64 || packet_len % 4 != 0 {
let next = find_magic_start(&hdr[1..]).map_or(HEADER_SIZE, |i| i + 1);
hdr.copy_within(next.., 0);
have = HEADER_SIZE - next;
continue;
}
have = 0;
let body_len = packet_len - HEADER_SIZE as u64;
if hdr[32..48] != self.set_id || &hdr[48..64] != TYPE_RECOVERY {
src.skip(body_len)?;
continue;
}
if body_len < 4 + self.slice_size {
scan.corrupt += 1;
src.skip(body_len)?;
continue;
}
let mut exp_buf = [0u8; 4];
src.read_exact(&mut exp_buf)?;
let exponent = u32::from_le_bytes(exp_buf);
let rest = body_len - 4;
if !self.wants(exponent) {
scan.seen += 1;
src.skip(rest)?;
continue;
}
let mut hasher = Md5::new();
hasher.update(&hdr[32..64]);
hasher.update(exp_buf);
let mut data = vec![0u8; self.slice_size as usize];
src.read_exact(&mut data)?;
hasher.update(&data);
src.skip_hashing(rest - self.slice_size, &mut hasher)?;
let digest: [u8; 16] = hasher.finalize().into();
if digest[..] != hdr[16..32] {
scan.corrupt += 1;
continue;
}
scan.seen += 1;
scan.kept += 1;
if self.blocks.len() >= self.limit {
self.evict_highest();
}
self.exponents.insert(exponent);
self.blocks.push(RecoveryBlock { exponent, data });
}
}
fn wants(&self, exponent: u32) -> bool {
if self.limit == 0 || self.exponents.contains(&exponent) {
return false;
}
self.blocks.len() < self.limit || self.exponents.last().is_some_and(|&max| exponent < max)
}
fn evict_highest(&mut self) {
let Some(max) = self.exponents.pop_last() else {
return;
};
if let Some(i) = self.blocks.iter().position(|b| b.exponent == max) {
self.blocks.remove(i);
}
}
}
enum Stop {
Eof,
Cap,
Io(io::Error),
}
struct Source<R> {
inner: R,
pos: u64,
cap: u64,
}
impl<R: Read> Source<R> {
fn new(inner: R, cap: u64) -> Self {
Self { inner, pos: 0, cap }
}
fn read_full(&mut self, buf: &mut [u8]) -> Result<usize, Stop> {
let mut n = 0;
while n < buf.len() {
let room = self.cap - self.pos;
if room == 0 {
return if self.at_eof()? {
Ok(n)
} else {
Err(Stop::Cap)
};
}
let want = (buf.len() - n).min(usize::try_from(room).unwrap_or(usize::MAX));
match self.inner.read(&mut buf[n..n + want]) {
Ok(0) => break,
Ok(k) => {
n += k;
self.pos += k as u64;
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(Stop::Io(e)),
}
}
Ok(n)
}
fn at_eof(&mut self) -> Result<bool, Stop> {
let mut probe = [0u8; 1];
loop {
match self.inner.read(&mut probe) {
Ok(0) => return Ok(true),
Ok(_) => return Ok(false),
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(Stop::Io(e)),
}
}
}
fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Stop> {
if self.read_full(buf)? < buf.len() {
return Err(Stop::Eof);
}
Ok(())
}
fn skip(&mut self, n: u64) -> Result<(), Stop> {
self.skip_with(n, |_| {})
}
fn skip_hashing(&mut self, n: u64, hasher: &mut Md5) -> Result<(), Stop> {
self.skip_with(n, |chunk| hasher.update(chunk))
}
fn skip_with(&mut self, mut n: u64, mut f: impl FnMut(&[u8])) -> Result<(), Stop> {
let mut buf = [0u8; 8192];
while n > 0 {
let want = n.min(buf.len() as u64) as usize;
self.read_exact(&mut buf[..want])?;
f(&buf[..want]);
n -= want as u64;
}
Ok(())
}
}
fn find_magic_start(buf: &[u8]) -> Option<usize> {
(0..buf.len()).find(|&i| starts_like_magic(&buf[i..]))
}
fn starts_like_magic(buf: &[u8]) -> bool {
let n = buf.len().min(MAGIC.len());
buf[..n] == MAGIC[..n]
}
pub fn load_recovery_blocks(dir: &Path, set_id: &[u8; 16], slice_size: u64) -> Vec<RecoveryBlock> {
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 Vec::new();
}
};
par2_files.sort();
let mut reader = RecoveryReader::new(*set_id, slice_size).with_max_volume_bytes(u64::MAX);
for par2_path in &par2_files {
let file = match std::fs::File::open(par2_path) {
Ok(f) => f,
Err(e) => {
debug!(file = %par2_path.display(), error = %e, "Skipping unreadable file");
continue;
}
};
let scan = reader.read_volume(io::BufReader::with_capacity(64 * 1024, file));
debug!(
file = %par2_path.display(),
count = scan.kept,
corrupt = scan.corrupt,
end = %scan.end,
"Loaded recovery blocks"
);
}
let mut blocks = reader.into_blocks();
blocks.sort_by_key(|b| b.exponent);
info!(total_blocks = blocks.len(), "Recovery blocks loaded");
blocks
}
#[cfg(test)]
mod tests {
use super::*;
const SET: [u8; 16] = [7u8; 16];
fn packet(set_id: &[u8; 16], ptype: &[u8; 16], body: &[u8]) -> Vec<u8> {
let mut hashed = Vec::new();
hashed.extend_from_slice(set_id);
hashed.extend_from_slice(ptype);
hashed.extend_from_slice(body);
let md5: [u8; 16] = Md5::digest(&hashed).into();
let mut p = Vec::new();
p.extend_from_slice(MAGIC);
p.extend_from_slice(&((HEADER_SIZE + body.len()) as u64).to_le_bytes());
p.extend_from_slice(&md5);
p.extend_from_slice(&hashed);
p
}
fn recv(exponent: u32, slice_size: usize) -> Vec<u8> {
let mut body = exponent.to_le_bytes().to_vec();
body.extend((0..slice_size).map(|i| (i as u32 ^ exponent) as u8));
packet(&SET, TYPE_RECOVERY, &body)
}
fn volume(exponents: &[u32], slice_size: usize) -> Vec<u8> {
exponents
.iter()
.flat_map(|&e| recv(e, slice_size))
.collect()
}
fn exps(r: &RecoveryReader) -> Vec<u32> {
r.blocks().iter().map(|b| b.exponent).collect()
}
#[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());
}
#[test]
fn packet_order_and_data() {
let vol = volume(&[5, 2, 9, 0], 16);
let mut r = RecoveryReader::new(SET, 16);
let scan = r.read_volume(&vol[..]);
assert!(scan.read_to_end());
assert_eq!((scan.seen, scan.kept, scan.corrupt), (4, 4, 0));
assert_eq!(scan.bytes_read, vol.len() as u64);
assert_eq!(exps(&r), [5, 2, 9, 0]);
assert_eq!(r.blocks()[1].data, recv(2, 16)[HEADER_SIZE + 4..]);
}
#[test]
fn budget_keeps_lowest_exponents_in_packet_order() {
let vol = volume(&[5, 2, 9, 0, 7, 1], 16);
let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::rows(3));
let scan = r.read_volume(&vol[..]);
assert!(scan.read_to_end());
assert_eq!(scan.seen, 6);
assert_eq!(r.seen(), 6);
assert_eq!(r.kept(), 3);
assert_eq!(exps(&r), [2, 0, 1]);
}
#[test]
fn byte_budget_and_shrinking() {
let vol = volume(&[3, 1, 2, 0], 16);
let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::bytes(16 * 3 + 5));
r.read_volume(&vol[..]);
assert_eq!(exps(&r), [1, 2, 0]);
r.set_budget(RecoveryBudget::rows(1));
assert_eq!(exps(&r), [0]);
}
#[test]
fn zero_budget_counts_without_keeping() {
let vol = volume(&[1, 2, 3], 16);
let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::rows(0));
let scan = r.read_volume(&vol[..]);
assert!(scan.read_to_end());
assert_eq!((scan.seen, scan.kept), (3, 0));
}
#[test]
fn accumulates_across_volumes_and_skips_duplicates() {
let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::rows(3));
r.read_volume(&volume(&[4, 5, 6], 16)[..]);
let scan = r.read_volume(&volume(&[5, 1], 16)[..]);
assert_eq!((scan.seen, scan.kept), (2, 1));
assert_eq!(exps(&r), [4, 5, 1]);
assert_eq!(r.seen(), 5);
}
#[test]
fn truncated_volume_is_not_complete() {
let vol = volume(&[0, 1, 2], 16);
for cut in [vol.len() - 1, vol.len() - 10, 2 * vol.len() / 3 + 3] {
let mut r = RecoveryReader::new(SET, 16);
let scan = r.read_volume(&vol[..cut]);
assert!(!scan.read_to_end(), "cut at {cut}");
assert!(matches!(scan.end, VolumeEnd::Truncated));
assert_eq!(exps(&r), [0, 1]);
}
let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::rows(1));
let scan = r.read_volume(&vol[..vol.len() - 3]);
assert!(matches!(scan.end, VolumeEnd::Truncated));
let mut r = RecoveryReader::new(SET, 16);
let one = recv(0, 16);
let scan = r.read_volume(&vol[..one.len() + 5]);
assert!(matches!(scan.end, VolumeEnd::Truncated));
}
#[test]
fn cap_reached_is_not_complete() {
let vol = volume(&[0, 1, 2], 16);
let one = recv(0, 16).len() as u64;
let mut r = RecoveryReader::new(SET, 16).with_max_volume_bytes(one * 2);
let scan = r.read_volume(&vol[..]);
assert!(matches!(scan.end, VolumeEnd::CapReached));
assert_eq!(scan.bytes_read, one * 2);
assert_eq!(exps(&r), [0, 1]);
let mut r = RecoveryReader::new(SET, 16).with_max_volume_bytes(vol.len() as u64);
assert!(r.read_volume(&vol[..]).read_to_end());
}
#[test]
fn io_error_is_reported() {
struct Failing<'a>(&'a [u8]);
impl Read for Failing<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.0.is_empty() {
return Err(io::Error::other("boom"));
}
let n = self.0.read(buf)?;
Ok(n)
}
}
let vol = volume(&[0, 1], 16);
let mut r = RecoveryReader::new(SET, 16);
let scan = r.read_volume(Failing(&vol[..vol.len() - 4]));
assert!(matches!(scan.end, VolumeEnd::Io(_)));
assert_eq!(exps(&r), [0]);
}
#[test]
fn skips_other_sets_types_garbage_and_corrupt() {
let mut vol = b"junk!".to_vec();
vol.extend(packet(&[9u8; 16], TYPE_RECOVERY, &[0u8; 20]));
vol.extend(packet(&SET, b"PAR 2.0\x00Creator\x00", b"test"));
vol.extend(recv(3, 16));
let mut bad = recv(4, 16);
let last = bad.len() - 1;
bad[last] ^= 1;
vol.extend(bad);
vol.extend(packet(&SET, TYPE_RECOVERY, &[0u8; 8])); vol.extend(recv(1, 16));
vol.extend(b"tail");
let mut r = RecoveryReader::new(SET, 16);
let scan = r.read_volume(&vol[..]);
assert!(scan.read_to_end(), "{}", scan.end);
assert_eq!((scan.seen, scan.kept, scan.corrupt), (2, 2, 2));
assert_eq!(exps(&r), [3, 1]);
}
#[test]
fn empty_input_is_complete() {
let mut r = RecoveryReader::new(SET, 16);
let scan = r.read_volume(&[][..]);
assert!(scan.read_to_end());
assert_eq!(scan.seen, 0);
}
}