Skip to main content

rust_par2/
recovery.rs

1//! Recovery block reading from PAR2 volume files.
2//!
3//! [`RecoveryReader`] streams RecoverySlice packets out of any [`Read`] source
4//! (a file, a network body, `&[u8]`), one volume at a time, keeping at most a
5//! budgeted number of rows and preferring the lowest exponents. Packets it will
6//! not keep are skipped without being buffered, so a volume much larger than
7//! the budget streams past in constant memory.
8//!
9//! [`load_recovery_blocks`] is the directory-scanning convenience wrapper.
10//!
11//! ```
12//! use rust_par2::recovery::{RecoveryBudget, RecoveryReader};
13//!
14//! # fn demo(set_id: [u8; 16], slice_size: u64, volumes: Vec<Vec<u8>>, missing: usize) {
15//! let mut reader = RecoveryReader::new(set_id, slice_size)
16//!     .with_budget(RecoveryBudget::rows(missing));
17//! for volume in &volumes {
18//!     let scan = reader.read_volume(&volume[..]);
19//!     if !scan.read_to_end() {
20//!         // This volume may carry more rows than we saw: don't conclude the
21//!         // set is unrepairable from it.
22//!     }
23//! }
24//! let rows = reader.into_blocks(); // feed to `Repair::new_owning`
25//! # let _ = rows; }
26//! ```
27
28use 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
38/// RecoverySlice packet type identifier.
39const TYPE_RECOVERY: &[u8; 16] = b"PAR 2.0\x00RecvSlic";
40
41/// Default cap on bytes read from one volume by a [`RecoveryReader`].
42pub const DEFAULT_MAX_VOLUME_BYTES: u64 = 512 * 1024 * 1024;
43
44/// A recovery block extracted from a RecoverySlice packet.
45#[derive(Debug, Clone)]
46pub struct RecoveryBlock {
47    /// The exponent (recovery block index) used in the Vandermonde matrix.
48    pub exponent: u32,
49    /// The recovery data. Length = slice_size.
50    pub data: Vec<u8>,
51}
52
53/// How many recovery rows a [`RecoveryReader`] may hold at once.
54///
55/// Both limits apply when both are set; the tighter one wins. Rows over the
56/// budget are dropped highest exponent first.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub struct RecoveryBudget {
59    /// Keep at most this many rows.
60    pub max_rows: Option<usize>,
61    /// Keep at most this many bytes of row data (`rows × slice_size`).
62    pub max_bytes: Option<u64>,
63}
64
65impl RecoveryBudget {
66    /// No limit: keep every row seen.
67    pub const UNLIMITED: Self = Self {
68        max_rows: None,
69        max_bytes: None,
70    };
71
72    /// Keep at most `n` rows.
73    pub fn rows(n: usize) -> Self {
74        Self {
75            max_rows: Some(n),
76            max_bytes: None,
77        }
78    }
79
80    /// Keep at most `bytes` of row data.
81    pub fn bytes(bytes: u64) -> Self {
82        Self {
83            max_rows: None,
84            max_bytes: Some(bytes),
85        }
86    }
87
88    /// Row limit for a set with this slice size.
89    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/// How reading one volume ended.
99#[derive(Debug)]
100pub enum VolumeEnd {
101    /// Read to the end of the stream: the volume carries no more rows than
102    /// were seen.
103    Complete,
104    /// The stream ended inside a packet. Rows after the cut are unknown.
105    Truncated,
106    /// Stopped at the per-volume byte cap with data left unread.
107    CapReached,
108    /// A read failed. Rows after the failure are unknown.
109    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/// What [`RecoveryReader::read_volume`] found in one volume.
124#[derive(Debug)]
125pub struct VolumeScan {
126    /// Intact RecoverySlice packets for this set found in the volume,
127    /// duplicates of exponents already held included. Packets skipped for
128    /// budget are counted without their hash being checked.
129    pub seen: usize,
130    /// Rows from this volume that were added to the reader. A later volume can
131    /// evict some of them again.
132    pub kept: usize,
133    /// RecoverySlice packets for this set dropped as damaged: a body too
134    /// short for the slice size, or a packet hash mismatch.
135    pub corrupt: usize,
136    /// Bytes consumed from the stream.
137    pub bytes_read: u64,
138    /// Whether the volume was read to its end.
139    pub end: VolumeEnd,
140}
141
142impl VolumeScan {
143    /// Whether the whole volume was read. When `false`, the volume may carry
144    /// rows that were never seen, so a shortfall must not be taken to mean
145    /// the set is unrepairable.
146    pub fn read_to_end(&self) -> bool {
147        matches!(self.end, VolumeEnd::Complete)
148    }
149}
150
151/// Streaming RecoverySlice reader for one recovery set, accumulating rows
152/// across volumes under a [`RecoveryBudget`].
153///
154/// Rows are held in the order their packets were read. When the budget is
155/// full, a new row is kept only if its exponent is lower than the highest one
156/// held, which is then dropped. Peak memory is one slice over the budget.
157#[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    /// Reader for recovery set `set_id` with slices of `slice_size` bytes, an
170    /// unlimited budget and the [`DEFAULT_MAX_VOLUME_BYTES`] cap.
171    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    /// Set the budget (builder form of [`set_budget`](Self::set_budget)).
184    pub fn with_budget(mut self, budget: RecoveryBudget) -> Self {
185        self.set_budget(budget);
186        self
187    }
188
189    /// Cap the bytes read from one volume. A volume longer than this reports
190    /// [`VolumeEnd::CapReached`].
191    pub fn with_max_volume_bytes(mut self, max: u64) -> Self {
192        self.max_volume_bytes = max;
193        self
194    }
195
196    /// Change the budget. Shrinking it drops the highest-exponent rows now.
197    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    /// Current row limit.
205    pub fn row_limit(&self) -> usize {
206        self.limit
207    }
208
209    /// Intact RecoverySlice packets for this set seen across all volumes.
210    pub fn seen(&self) -> usize {
211        self.seen
212    }
213
214    /// Rows held now.
215    pub fn kept(&self) -> usize {
216        self.blocks.len()
217    }
218
219    /// Rows held, in packet order.
220    pub fn blocks(&self) -> &[RecoveryBlock] {
221        &self.blocks
222    }
223
224    /// Take the held rows, in packet order.
225    pub fn into_blocks(self) -> Vec<RecoveryBlock> {
226        self.blocks
227    }
228
229    /// Read RecoverySlice packets from one volume.
230    ///
231    /// Packets for other sets, other packet types and rows outside the budget
232    /// are read past without being buffered. Never fails: how the read ended
233    /// is in [`VolumeScan::end`], and rows found before a failure are kept.
234    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                // End of stream between packets. A partial packet header is
262                // a cut; anything else is trailing garbage.
263                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                // Resync: keep the window from the next possible magic start.
272                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    /// Whether a new row with this exponent would be kept.
332    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
349/// Why a volume scan stopped before a clean end.
350enum Stop {
351    Eof,
352    Cap,
353    Io(io::Error),
354}
355
356/// A reader that counts bytes and refuses to go past a cap.
357struct 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    /// Read until `buf` is full or the stream ends; returns bytes read.
369    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    /// At the cap: probe whether the stream has anything left.
395    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
434/// Offset of the first position in `buf` where the packet magic could start
435/// (a full match, or a prefix of it running to the end of `buf`).
436fn 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
445/// Load all recovery blocks from PAR2 files in a directory.
446///
447/// Scans all `.par2` files (index and volumes) for RecoverySlice packets
448/// matching the given recovery set ID. Returns blocks sorted by exponent.
449pub fn load_recovery_blocks(dir: &Path, set_id: &[u8; 16], slice_size: u64) -> Vec<RecoveryBlock> {
450    // Find all .par2 files in the directory
451    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        // Cut inside a skipped (over-budget) packet is still a truncation.
603        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        // Cut inside the next packet's header.
607        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        // A volume exactly at the cap is complete.
624        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])); // too short
658        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}