1use std::collections::BTreeSet;
29use std::fmt;
30use std::io::{self, Read};
31use std::path::Path;
32
33use md5::{Digest, Md5};
34use tracing::{debug, info, warn};
35
36use crate::packets::{HEADER_SIZE, MAGIC};
37
38const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
40
41pub const DEFAULT_MAX_VOLUME_BYTES: u64 = 512 * 1024 * 1024;
43
44#[derive(Debug, Clone)]
46pub struct RecoveryBlock {
47 pub exponent: u32,
49 pub data: Vec<u8>,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub struct RecoveryBudget {
59 pub max_rows: Option<usize>,
61 pub max_bytes: Option<u64>,
63}
64
65impl RecoveryBudget {
66 pub const UNLIMITED: Self = Self {
68 max_rows: None,
69 max_bytes: None,
70 };
71
72 pub fn rows(n: usize) -> Self {
74 Self {
75 max_rows: Some(n),
76 max_bytes: None,
77 }
78 }
79
80 pub fn bytes(bytes: u64) -> Self {
82 Self {
83 max_rows: None,
84 max_bytes: Some(bytes),
85 }
86 }
87
88 pub fn row_limit(&self, slice_size: u64) -> usize {
90 let by_bytes = match self.max_bytes {
91 Some(b) if slice_size > 0 => usize::try_from(b / slice_size).unwrap_or(usize::MAX),
92 _ => usize::MAX,
93 };
94 self.max_rows.unwrap_or(usize::MAX).min(by_bytes)
95 }
96}
97
98#[derive(Debug)]
100pub enum VolumeEnd {
101 Complete,
104 Truncated,
106 CapReached,
108 Io(io::Error),
110}
111
112impl fmt::Display for VolumeEnd {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match self {
115 Self::Complete => write!(f, "read to end"),
116 Self::Truncated => write!(f, "truncated inside a packet"),
117 Self::CapReached => write!(f, "stopped at the per-volume byte cap"),
118 Self::Io(e) => write!(f, "read error: {e}"),
119 }
120 }
121}
122
123#[derive(Debug)]
125pub struct VolumeScan {
126 pub seen: usize,
130 pub kept: usize,
133 pub corrupt: usize,
136 pub bytes_read: u64,
138 pub end: VolumeEnd,
140}
141
142impl VolumeScan {
143 pub fn read_to_end(&self) -> bool {
147 matches!(self.end, VolumeEnd::Complete)
148 }
149}
150
151#[derive(Debug, Clone)]
158pub struct RecoveryReader {
159 set_id: [u8; 16],
160 slice_size: u64,
161 limit: usize,
162 max_volume_bytes: u64,
163 blocks: Vec<RecoveryBlock>,
164 exponents: BTreeSet<u32>,
165 seen: usize,
166}
167
168impl RecoveryReader {
169 pub fn new(set_id: [u8; 16], slice_size: u64) -> Self {
172 Self {
173 set_id,
174 slice_size,
175 limit: usize::MAX,
176 max_volume_bytes: DEFAULT_MAX_VOLUME_BYTES,
177 blocks: Vec::new(),
178 exponents: BTreeSet::new(),
179 seen: 0,
180 }
181 }
182
183 pub fn with_budget(mut self, budget: RecoveryBudget) -> Self {
185 self.set_budget(budget);
186 self
187 }
188
189 pub fn with_max_volume_bytes(mut self, max: u64) -> Self {
192 self.max_volume_bytes = max;
193 self
194 }
195
196 pub fn set_budget(&mut self, budget: RecoveryBudget) {
198 self.limit = budget.row_limit(self.slice_size);
199 while self.blocks.len() > self.limit {
200 self.evict_highest();
201 }
202 }
203
204 pub fn row_limit(&self) -> usize {
206 self.limit
207 }
208
209 pub fn seen(&self) -> usize {
211 self.seen
212 }
213
214 pub fn kept(&self) -> usize {
216 self.blocks.len()
217 }
218
219 pub fn blocks(&self) -> &[RecoveryBlock] {
221 &self.blocks
222 }
223
224 pub fn into_blocks(self) -> Vec<RecoveryBlock> {
226 self.blocks
227 }
228
229 pub fn read_volume<R: Read>(&mut self, reader: R) -> VolumeScan {
235 let mut src = Source::new(reader, self.max_volume_bytes);
236 let mut scan = VolumeScan {
237 seen: 0,
238 kept: 0,
239 corrupt: 0,
240 bytes_read: 0,
241 end: VolumeEnd::Complete,
242 };
243 scan.end = match self.scan(&mut src, &mut scan) {
244 Ok(()) => VolumeEnd::Complete,
245 Err(Stop::Eof) => VolumeEnd::Truncated,
246 Err(Stop::Cap) => VolumeEnd::CapReached,
247 Err(Stop::Io(e)) => VolumeEnd::Io(e),
248 };
249 scan.bytes_read = src.pos;
250 self.seen += scan.seen;
251 scan
252 }
253
254 fn scan<R: Read>(&mut self, src: &mut Source<R>, scan: &mut VolumeScan) -> Result<(), Stop> {
255 let mut hdr = [0u8; HEADER_SIZE];
256 let mut have = 0;
257 loop {
258 let got = src.read_full(&mut hdr[have..])?;
259 have += got;
260 if have < HEADER_SIZE {
261 return if have > 0 && starts_like_magic(&hdr[..have]) {
264 Err(Stop::Eof)
265 } else {
266 Ok(())
267 };
268 }
269
270 if &hdr[0..8] != MAGIC {
271 let next = find_magic_start(&hdr[1..]).map_or(HEADER_SIZE, |i| i + 1);
273 hdr.copy_within(next.., 0);
274 have = HEADER_SIZE - next;
275 continue;
276 }
277 let packet_len = u64::from_le_bytes(hdr[8..16].try_into().unwrap());
278 if packet_len < HEADER_SIZE as u64 || packet_len % 4 != 0 {
279 let next = find_magic_start(&hdr[1..]).map_or(HEADER_SIZE, |i| i + 1);
280 hdr.copy_within(next.., 0);
281 have = HEADER_SIZE - next;
282 continue;
283 }
284 have = 0;
285 let body_len = packet_len - HEADER_SIZE as u64;
286
287 if hdr[32..48] != self.set_id || &hdr[48..64] != TYPE_RECOVERY {
288 src.skip(body_len)?;
289 continue;
290 }
291 if body_len < 4 + self.slice_size {
292 scan.corrupt += 1;
293 src.skip(body_len)?;
294 continue;
295 }
296
297 let mut exp_buf = [0u8; 4];
298 src.read_exact(&mut exp_buf)?;
299 let exponent = u32::from_le_bytes(exp_buf);
300 let rest = body_len - 4;
301
302 if !self.wants(exponent) {
303 scan.seen += 1;
304 src.skip(rest)?;
305 continue;
306 }
307
308 let mut hasher = Md5::new();
309 hasher.update(&hdr[32..64]);
310 hasher.update(exp_buf);
311 let mut data = vec![0u8; self.slice_size as usize];
312 src.read_exact(&mut data)?;
313 hasher.update(&data);
314 src.skip_hashing(rest - self.slice_size, &mut hasher)?;
315 let digest: [u8; 16] = hasher.finalize().into();
316 if digest[..] != hdr[16..32] {
317 scan.corrupt += 1;
318 continue;
319 }
320
321 scan.seen += 1;
322 scan.kept += 1;
323 if self.blocks.len() >= self.limit {
324 self.evict_highest();
325 }
326 self.exponents.insert(exponent);
327 self.blocks.push(RecoveryBlock { exponent, data });
328 }
329 }
330
331 fn wants(&self, exponent: u32) -> bool {
333 if self.limit == 0 || self.exponents.contains(&exponent) {
334 return false;
335 }
336 self.blocks.len() < self.limit || self.exponents.last().is_some_and(|&max| exponent < max)
337 }
338
339 fn evict_highest(&mut self) {
340 let Some(max) = self.exponents.pop_last() else {
341 return;
342 };
343 if let Some(i) = self.blocks.iter().position(|b| b.exponent == max) {
344 self.blocks.remove(i);
345 }
346 }
347}
348
349enum Stop {
351 Eof,
352 Cap,
353 Io(io::Error),
354}
355
356struct Source<R> {
358 inner: R,
359 pos: u64,
360 cap: u64,
361}
362
363impl<R: Read> Source<R> {
364 fn new(inner: R, cap: u64) -> Self {
365 Self { inner, pos: 0, cap }
366 }
367
368 fn read_full(&mut self, buf: &mut [u8]) -> Result<usize, Stop> {
370 let mut n = 0;
371 while n < buf.len() {
372 let room = self.cap - self.pos;
373 if room == 0 {
374 return if self.at_eof()? {
375 Ok(n)
376 } else {
377 Err(Stop::Cap)
378 };
379 }
380 let want = (buf.len() - n).min(usize::try_from(room).unwrap_or(usize::MAX));
381 match self.inner.read(&mut buf[n..n + want]) {
382 Ok(0) => break,
383 Ok(k) => {
384 n += k;
385 self.pos += k as u64;
386 }
387 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
388 Err(e) => return Err(Stop::Io(e)),
389 }
390 }
391 Ok(n)
392 }
393
394 fn at_eof(&mut self) -> Result<bool, Stop> {
396 let mut probe = [0u8; 1];
397 loop {
398 match self.inner.read(&mut probe) {
399 Ok(0) => return Ok(true),
400 Ok(_) => return Ok(false),
401 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
402 Err(e) => return Err(Stop::Io(e)),
403 }
404 }
405 }
406
407 fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Stop> {
408 if self.read_full(buf)? < buf.len() {
409 return Err(Stop::Eof);
410 }
411 Ok(())
412 }
413
414 fn skip(&mut self, n: u64) -> Result<(), Stop> {
415 self.skip_with(n, |_| {})
416 }
417
418 fn skip_hashing(&mut self, n: u64, hasher: &mut Md5) -> Result<(), Stop> {
419 self.skip_with(n, |chunk| hasher.update(chunk))
420 }
421
422 fn skip_with(&mut self, mut n: u64, mut f: impl FnMut(&[u8])) -> Result<(), Stop> {
423 let mut buf = [0u8; 8192];
424 while n > 0 {
425 let want = n.min(buf.len() as u64) as usize;
426 self.read_exact(&mut buf[..want])?;
427 f(&buf[..want]);
428 n -= want as u64;
429 }
430 Ok(())
431 }
432}
433
434fn find_magic_start(buf: &[u8]) -> Option<usize> {
437 (0..buf.len()).find(|&i| starts_like_magic(&buf[i..]))
438}
439
440fn starts_like_magic(buf: &[u8]) -> bool {
441 let n = buf.len().min(MAGIC.len());
442 buf[..n] == MAGIC[..n]
443}
444
445pub fn load_recovery_blocks(dir: &Path, set_id: &[u8; 16], slice_size: u64) -> Vec<RecoveryBlock> {
450 let mut par2_files: Vec<_> = match std::fs::read_dir(dir) {
452 Ok(entries) => entries
453 .filter_map(|e| e.ok())
454 .map(|e| e.path())
455 .filter(|p| {
456 p.extension()
457 .is_some_and(|ext| ext.eq_ignore_ascii_case("par2"))
458 })
459 .collect(),
460 Err(e) => {
461 warn!(error = %e, "Failed to read directory for recovery files");
462 return Vec::new();
463 }
464 };
465 par2_files.sort();
466
467 let mut reader = RecoveryReader::new(*set_id, slice_size).with_max_volume_bytes(u64::MAX);
468 for par2_path in &par2_files {
469 let file = match std::fs::File::open(par2_path) {
470 Ok(f) => f,
471 Err(e) => {
472 debug!(file = %par2_path.display(), error = %e, "Skipping unreadable file");
473 continue;
474 }
475 };
476 let scan = reader.read_volume(io::BufReader::with_capacity(64 * 1024, file));
477 debug!(
478 file = %par2_path.display(),
479 count = scan.kept,
480 corrupt = scan.corrupt,
481 end = %scan.end,
482 "Loaded recovery blocks"
483 );
484 }
485
486 let mut blocks = reader.into_blocks();
487 blocks.sort_by_key(|b| b.exponent);
488
489 info!(total_blocks = blocks.len(), "Recovery blocks loaded");
490
491 blocks
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 const SET: [u8; 16] = [7u8; 16];
499
500 fn packet(set_id: &[u8; 16], ptype: &[u8; 16], body: &[u8]) -> Vec<u8> {
501 let mut hashed = Vec::new();
502 hashed.extend_from_slice(set_id);
503 hashed.extend_from_slice(ptype);
504 hashed.extend_from_slice(body);
505 let md5: [u8; 16] = Md5::digest(&hashed).into();
506 let mut p = Vec::new();
507 p.extend_from_slice(MAGIC);
508 p.extend_from_slice(&((HEADER_SIZE + body.len()) as u64).to_le_bytes());
509 p.extend_from_slice(&md5);
510 p.extend_from_slice(&hashed);
511 p
512 }
513
514 fn recv(exponent: u32, slice_size: usize) -> Vec<u8> {
515 let mut body = exponent.to_le_bytes().to_vec();
516 body.extend((0..slice_size).map(|i| (i as u32 ^ exponent) as u8));
517 packet(&SET, TYPE_RECOVERY, &body)
518 }
519
520 fn volume(exponents: &[u32], slice_size: usize) -> Vec<u8> {
521 exponents
522 .iter()
523 .flat_map(|&e| recv(e, slice_size))
524 .collect()
525 }
526
527 fn exps(r: &RecoveryReader) -> Vec<u32> {
528 r.blocks().iter().map(|b| b.exponent).collect()
529 }
530
531 #[test]
532 fn test_load_empty_dir() {
533 let dir = tempfile::tempdir().unwrap();
534 let set_id = [0u8; 16];
535 let blocks = load_recovery_blocks(dir.path(), &set_id, 768000);
536 assert!(blocks.is_empty());
537 }
538
539 #[test]
540 fn packet_order_and_data() {
541 let vol = volume(&[5, 2, 9, 0], 16);
542 let mut r = RecoveryReader::new(SET, 16);
543 let scan = r.read_volume(&vol[..]);
544 assert!(scan.read_to_end());
545 assert_eq!((scan.seen, scan.kept, scan.corrupt), (4, 4, 0));
546 assert_eq!(scan.bytes_read, vol.len() as u64);
547 assert_eq!(exps(&r), [5, 2, 9, 0]);
548 assert_eq!(r.blocks()[1].data, recv(2, 16)[HEADER_SIZE + 4..]);
549 }
550
551 #[test]
552 fn budget_keeps_lowest_exponents_in_packet_order() {
553 let vol = volume(&[5, 2, 9, 0, 7, 1], 16);
554 let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::rows(3));
555 let scan = r.read_volume(&vol[..]);
556 assert!(scan.read_to_end());
557 assert_eq!(scan.seen, 6);
558 assert_eq!(r.seen(), 6);
559 assert_eq!(r.kept(), 3);
560 assert_eq!(exps(&r), [2, 0, 1]);
561 }
562
563 #[test]
564 fn byte_budget_and_shrinking() {
565 let vol = volume(&[3, 1, 2, 0], 16);
566 let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::bytes(16 * 3 + 5));
567 r.read_volume(&vol[..]);
568 assert_eq!(exps(&r), [1, 2, 0]);
569 r.set_budget(RecoveryBudget::rows(1));
570 assert_eq!(exps(&r), [0]);
571 }
572
573 #[test]
574 fn zero_budget_counts_without_keeping() {
575 let vol = volume(&[1, 2, 3], 16);
576 let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::rows(0));
577 let scan = r.read_volume(&vol[..]);
578 assert!(scan.read_to_end());
579 assert_eq!((scan.seen, scan.kept), (3, 0));
580 }
581
582 #[test]
583 fn accumulates_across_volumes_and_skips_duplicates() {
584 let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::rows(3));
585 r.read_volume(&volume(&[4, 5, 6], 16)[..]);
586 let scan = r.read_volume(&volume(&[5, 1], 16)[..]);
587 assert_eq!((scan.seen, scan.kept), (2, 1));
588 assert_eq!(exps(&r), [4, 5, 1]);
589 assert_eq!(r.seen(), 5);
590 }
591
592 #[test]
593 fn truncated_volume_is_not_complete() {
594 let vol = volume(&[0, 1, 2], 16);
595 for cut in [vol.len() - 1, vol.len() - 10, 2 * vol.len() / 3 + 3] {
596 let mut r = RecoveryReader::new(SET, 16);
597 let scan = r.read_volume(&vol[..cut]);
598 assert!(!scan.read_to_end(), "cut at {cut}");
599 assert!(matches!(scan.end, VolumeEnd::Truncated));
600 assert_eq!(exps(&r), [0, 1]);
601 }
602 let mut r = RecoveryReader::new(SET, 16).with_budget(RecoveryBudget::rows(1));
604 let scan = r.read_volume(&vol[..vol.len() - 3]);
605 assert!(matches!(scan.end, VolumeEnd::Truncated));
606 let mut r = RecoveryReader::new(SET, 16);
608 let one = recv(0, 16);
609 let scan = r.read_volume(&vol[..one.len() + 5]);
610 assert!(matches!(scan.end, VolumeEnd::Truncated));
611 }
612
613 #[test]
614 fn cap_reached_is_not_complete() {
615 let vol = volume(&[0, 1, 2], 16);
616 let one = recv(0, 16).len() as u64;
617 let mut r = RecoveryReader::new(SET, 16).with_max_volume_bytes(one * 2);
618 let scan = r.read_volume(&vol[..]);
619 assert!(matches!(scan.end, VolumeEnd::CapReached));
620 assert_eq!(scan.bytes_read, one * 2);
621 assert_eq!(exps(&r), [0, 1]);
622
623 let mut r = RecoveryReader::new(SET, 16).with_max_volume_bytes(vol.len() as u64);
625 assert!(r.read_volume(&vol[..]).read_to_end());
626 }
627
628 #[test]
629 fn io_error_is_reported() {
630 struct Failing<'a>(&'a [u8]);
631 impl Read for Failing<'_> {
632 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
633 if self.0.is_empty() {
634 return Err(io::Error::other("boom"));
635 }
636 let n = self.0.read(buf)?;
637 Ok(n)
638 }
639 }
640 let vol = volume(&[0, 1], 16);
641 let mut r = RecoveryReader::new(SET, 16);
642 let scan = r.read_volume(Failing(&vol[..vol.len() - 4]));
643 assert!(matches!(scan.end, VolumeEnd::Io(_)));
644 assert_eq!(exps(&r), [0]);
645 }
646
647 #[test]
648 fn skips_other_sets_types_garbage_and_corrupt() {
649 let mut vol = b"junk!".to_vec();
650 vol.extend(packet(&[9u8; 16], TYPE_RECOVERY, &[0u8; 20]));
651 vol.extend(packet(&SET, b"PAR 2.0\x00Creator\x00", b"test"));
652 vol.extend(recv(3, 16));
653 let mut bad = recv(4, 16);
654 let last = bad.len() - 1;
655 bad[last] ^= 1;
656 vol.extend(bad);
657 vol.extend(packet(&SET, TYPE_RECOVERY, &[0u8; 8])); vol.extend(recv(1, 16));
659 vol.extend(b"tail");
660
661 let mut r = RecoveryReader::new(SET, 16);
662 let scan = r.read_volume(&vol[..]);
663 assert!(scan.read_to_end(), "{}", scan.end);
664 assert_eq!((scan.seen, scan.kept, scan.corrupt), (2, 2, 2));
665 assert_eq!(exps(&r), [3, 1]);
666 }
667
668 #[test]
669 fn empty_input_is_complete() {
670 let mut r = RecoveryReader::new(SET, 16);
671 let scan = r.read_volume(&[][..]);
672 assert!(scan.read_to_end());
673 assert_eq!(scan.seen, 0);
674 }
675}