Skip to main content

par2_rs/packet/
mod.rs

1pub mod budget;
2pub mod creator;
3pub(crate) mod encode;
4pub mod file_desc;
5pub mod file_verify;
6pub mod header;
7pub mod main;
8pub mod recovery;
9
10use std::fs::File;
11use std::io::{self, BufRead, BufReader, Read, Seek, SeekFrom};
12use std::path::Path;
13use std::sync::Arc;
14
15use tracing::{debug, trace, warn};
16
17use crate::checksum::Md5State;
18use crate::error::{Par2Error, Result};
19use crate::types::{CancellationToken, MAX_FILES_PER_SET, RecoverySetId};
20
21pub use budget::{
22    DEFAULT_MAX_EXAMINED_PACKETS, DEFAULT_MAX_RETAINED_METADATA_BYTES,
23    DEFAULT_MAX_RETAINED_PACKETS, MAX_RECOVERY_EXPONENT, PacketScanBudget, PacketScanLimits,
24    RECOVERY_EXPONENT_DOMAIN,
25};
26
27const MAX_MAIN_BODY_BYTES: usize = 12 + MAX_FILES_PER_SET * 16;
28const MAX_FILE_DESC_BODY_BYTES: usize = 56 + 100_000;
29const MAX_IFSC_BODY_BYTES: usize = 16 + 32_768 * 20;
30const MAX_CREATOR_BODY_BYTES: usize = 100_000;
31
32pub use creator::CreatorPacket;
33pub use file_desc::FileDescriptionPacket;
34pub use file_verify::IfscPacket;
35pub use header::{HEADER_SIZE, MAGIC, PacketHeader, PacketType};
36pub use main::MainPacket;
37pub use recovery::{RecoverySliceData, RecoverySlicePacket};
38
39/// A parsed PAR2 packet (any type).
40#[derive(Debug, Clone)]
41pub enum Packet {
42    Main(MainPacket),
43    FileDescription(FileDescriptionPacket),
44    InputFileSliceChecksum(IfscPacket),
45    RecoverySlice(RecoverySlicePacket),
46    Creator(CreatorPacket),
47    Unknown {
48        packet_type: [u8; 16],
49        body: Vec<u8>,
50    },
51}
52
53#[derive(Debug, Clone)]
54pub struct ScannedPacket {
55    pub packet: Packet,
56    pub offset: u64,
57    pub recovery_set_id: RecoverySetId,
58}
59
60/// Where a bounded scan delivers each accepted packet.
61///
62/// The scanners hand packets over one at a time instead of returning a vector,
63/// so a caller that deduplicates — [`crate::par2_set::Par2FileSet`]'s builder,
64/// or the repairer's inventory loader — can drop a packet the moment it decides
65/// not to keep it. Nothing accumulates on the scanner's side, so the peak is
66/// whatever the sink itself retains, and that is what the shared
67/// [`PacketScanBudget`] meters.
68///
69/// Returning an error aborts the scan; the error reaches the caller unchanged.
70pub trait PacketSink {
71    fn accept(&mut self, packet: Packet, offset: u64, recovery_set_id: RecoverySetId)
72    -> Result<()>;
73}
74
75impl<F> PacketSink for F
76where
77    F: FnMut(Packet, u64, RecoverySetId) -> Result<()>,
78{
79    fn accept(
80        &mut self,
81        packet: Packet,
82        offset: u64,
83        recovery_set_id: RecoverySetId,
84    ) -> Result<()> {
85        self(packet, offset, recovery_set_id)
86    }
87}
88
89/// Sink that keeps every packet it is handed, charging each one to the budget.
90///
91/// This is the shape the vector-returning scanners are built from. It applies
92/// no deduplication, so its retained-packet meter counts the raw stream.
93struct CollectingSink<'a> {
94    budget: &'a PacketScanBudget,
95    packets: Vec<ScannedPacket>,
96}
97
98impl<'a> CollectingSink<'a> {
99    fn new(budget: &'a PacketScanBudget) -> Self {
100        Self {
101            budget,
102            packets: Vec::new(),
103        }
104    }
105}
106
107impl PacketSink for CollectingSink<'_> {
108    fn accept(
109        &mut self,
110        packet: Packet,
111        offset: u64,
112        recovery_set_id: RecoverySetId,
113    ) -> Result<()> {
114        self.budget
115            .charge_retained(budget::packet_retained_bytes(&packet))?;
116        // `Vec` growth is amortised doubling, so charge the slot the packet is
117        // about to occupy on top of the packet's own metadata.
118        self.budget.charge_bytes(size_of::<ScannedPacket>())?;
119        budget::reserve_fallible(&mut self.packets, 1)?;
120        self.packets.push(ScannedPacket {
121            packet,
122            offset,
123            recovery_set_id,
124        });
125        Ok(())
126    }
127}
128
129/// Parse a single packet from a byte slice that starts at the packet header.
130///
131/// Returns the parsed packet and the number of bytes consumed.
132/// `offset` is used for error reporting (position in the file/stream).
133fn parse_packet_internal(
134    data: &[u8],
135    offset: u64,
136    recovery_path: Option<&Arc<Path>>,
137) -> Result<(Packet, usize)> {
138    let header = PacketHeader::parse(data, offset)?;
139    let total_len =
140        usize::try_from(header.length).map_err(|_| Par2Error::ResourceLimitExceeded {
141            reason: format!("packet length {} exceeds addressable memory", header.length),
142        })?;
143
144    if data.len() < total_len {
145        return Err(Par2Error::PacketTooShort {
146            expected: header.length,
147            actual: data.len() as u64,
148        });
149    }
150
151    // Validate packet hash
152    header.validate_hash(&data[..total_len], offset)?;
153
154    let body = &data[HEADER_SIZE..total_len];
155
156    let packet = match header.packet_type {
157        PacketType::Main => {
158            debug!("parsed Main packet at offset {offset}");
159            Packet::Main(MainPacket::parse(body, header.recovery_set_id)?)
160        }
161        PacketType::FileDescription => {
162            debug!("parsed FileDescription packet at offset {offset}");
163            Packet::FileDescription(FileDescriptionPacket::parse(body)?)
164        }
165        PacketType::InputFileSliceChecksum => {
166            debug!("parsed IFSC packet at offset {offset}");
167            Packet::InputFileSliceChecksum(IfscPacket::parse(body)?)
168        }
169        PacketType::RecoverySlice => {
170            debug!("parsed RecoverySlice packet at offset {offset}");
171            if let Some(path) = recovery_path {
172                if body.len() <= 4 {
173                    return Err(Par2Error::InvalidRecoveryPacket {
174                        reason: format!("body too short: {} bytes, need more than 4", body.len()),
175                    });
176                }
177                let exponent = u32::from_le_bytes(body[0..4].try_into().unwrap());
178                Packet::RecoverySlice(RecoverySlicePacket {
179                    exponent,
180                    data: RecoverySliceData::file_backed_shared(
181                        Arc::clone(path),
182                        offset + HEADER_SIZE as u64 + 4,
183                        body.len() - 4,
184                        None,
185                    ),
186                })
187            } else {
188                Packet::RecoverySlice(RecoverySlicePacket::parse(body)?)
189            }
190        }
191        PacketType::Creator => {
192            debug!("parsed Creator packet at offset {offset}");
193            Packet::Creator(CreatorPacket::parse(body)?)
194        }
195        PacketType::Unknown(sig) => {
196            debug!("parsed Unknown packet type at offset {offset}");
197            Packet::Unknown {
198                packet_type: sig,
199                body: body.to_vec(),
200            }
201        }
202    };
203
204    Ok((packet, total_len))
205}
206
207pub fn parse_packet(data: &[u8], offset: u64) -> Result<(Packet, usize)> {
208    parse_packet_internal(data, offset, None)
209}
210
211fn parse_packet_body(header: &PacketHeader, body: Vec<u8>) -> Result<Packet> {
212    Ok(match header.packet_type {
213        PacketType::Main => Packet::Main(MainPacket::parse(&body, header.recovery_set_id)?),
214        PacketType::FileDescription => {
215            Packet::FileDescription(FileDescriptionPacket::parse(&body)?)
216        }
217        PacketType::InputFileSliceChecksum => {
218            Packet::InputFileSliceChecksum(IfscPacket::parse(&body)?)
219        }
220        PacketType::RecoverySlice => Packet::RecoverySlice(RecoverySlicePacket::parse(&body)?),
221        PacketType::Creator => Packet::Creator(CreatorPacket::parse(&body)?),
222        PacketType::Unknown(sig) => Packet::Unknown {
223            packet_type: sig,
224            body,
225        },
226    })
227}
228
229/// Scan an in-memory byte stream for PAR2 packets under the default limits.
230///
231/// Scans through `data` looking for valid PAR2 packets. When a valid packet is
232/// found it is parsed and collected; when invalid data is encountered the scan
233/// steps forward byte by byte looking for the next magic sequence.
234///
235/// `base_offset` is the offset of `data[0]` in the original file (for error
236/// reporting).
237///
238/// The result is complete or it is an error. A stream that would exceed
239/// [`PacketScanLimits::default`] yields [`Par2Error::ResourceLimitExceeded`]
240/// rather than a silently truncated vector — a caller cannot tell a truncated
241/// inventory from a small one, and acting on a truncated inventory means
242/// repairing from recovery data that was quietly dropped.
243pub fn scan_packets(data: &[u8], base_offset: u64) -> Result<Vec<(Packet, u64)>> {
244    scan_packets_with_limits(data, base_offset, PacketScanLimits::default())
245}
246
247/// [`scan_packets`] under caller-chosen limits.
248pub fn scan_packets_with_limits(
249    data: &[u8],
250    base_offset: u64,
251    limits: PacketScanLimits,
252) -> Result<Vec<(Packet, u64)>> {
253    let budget = PacketScanBudget::new(limits);
254    let mut sink = CollectingSink::new(&budget);
255    scan_packets_bounded(data, base_offset, &budget, &mut sink)?;
256    Ok(sink
257        .packets
258        .into_iter()
259        .map(|scanned| (scanned.packet, scanned.offset))
260        .collect())
261}
262
263/// Stream the packets of an in-memory byte range into `sink` under `budget`.
264///
265/// Nothing accumulates here: each packet is parsed, charged to the budget's
266/// examined meter, and handed straight to the sink.
267pub fn scan_packets_bounded(
268    data: &[u8],
269    base_offset: u64,
270    budget: &PacketScanBudget,
271    sink: &mut dyn PacketSink,
272) -> Result<()> {
273    scan_packets_internal(data, base_offset, None, budget, sink)
274}
275
276fn scan_packets_internal(
277    data: &[u8],
278    base_offset: u64,
279    recovery_path: Option<&Arc<Path>>,
280    budget: &PacketScanBudget,
281    sink: &mut dyn PacketSink,
282) -> Result<()> {
283    let mut pos = 0;
284
285    while pos + HEADER_SIZE <= data.len() {
286        budget.check_cancelled()?;
287        // Try to parse a packet at the current position
288        let offset = base_offset + pos as u64;
289
290        match parse_packet_internal(&data[pos..], offset, recovery_path) {
291            Ok((packet, consumed)) => {
292                trace!("packet at offset {offset}, size {consumed}");
293                budget.charge_examined()?;
294                // Unknown packet bodies are never usable, and their length is
295                // bounded only by the input, so drop them here exactly as the
296                // streaming path does rather than handing a sink something it
297                // would only throw away.
298                match packet {
299                    Packet::Unknown { packet_type, .. } => {
300                        debug!(
301                            "ignoring unknown packet type {packet_type:02x?} at offset {offset}"
302                        );
303                    }
304                    packet => {
305                        let recovery_set_id = header_recovery_set_id(&data[pos..]);
306                        sink.accept(packet, offset, recovery_set_id)?;
307                    }
308                }
309                pos += consumed;
310            }
311            Err(Par2Error::ResourceLimitExceeded { reason }) => {
312                return Err(Par2Error::ResourceLimitExceeded { reason });
313            }
314            Err(Par2Error::Cancelled) => return Err(Par2Error::Cancelled),
315            Err(_) => {
316                // Scan forward to find the next magic sequence
317                match find_next_magic(&data[pos + 1..]) {
318                    Some(skip) => {
319                        let skipped = skip + 1;
320                        warn!("skipped {skipped} bytes at offset {offset} looking for next packet");
321                        pos += skipped;
322                    }
323                    None => {
324                        // No more magic sequences found
325                        break;
326                    }
327                }
328            }
329        }
330    }
331
332    Ok(())
333}
334
335/// The recovery set ID of a packet whose header has already parsed cleanly.
336fn header_recovery_set_id(packet: &[u8]) -> RecoverySetId {
337    let mut bytes = [0u8; 16];
338    bytes.copy_from_slice(&packet[32..48]);
339    RecoverySetId::from_bytes(bytes)
340}
341
342fn find_next_magic_in_reader(
343    reader: &mut BufReader<File>,
344    offset: &mut u64,
345    budget: &PacketScanBudget,
346) -> Result<Option<u64>> {
347    let mut matched = 0usize;
348
349    loop {
350        budget.check_cancelled()?;
351        let mut found = None;
352        let mut consumed = 0usize;
353
354        {
355            let buf = reader.fill_buf()?;
356            if buf.is_empty() {
357                return Ok(None);
358            }
359
360            while consumed < buf.len() {
361                let byte = buf[consumed];
362                if byte == MAGIC[matched] {
363                    matched += 1;
364                    if matched == MAGIC.len() {
365                        found = Some(*offset + consumed as u64 + 1 - MAGIC.len() as u64);
366                        consumed += 1;
367                        break;
368                    }
369                } else {
370                    matched = if byte == MAGIC[0] { 1 } else { 0 };
371                }
372                consumed += 1;
373            }
374        }
375
376        reader.consume(consumed);
377        *offset += consumed as u64;
378
379        if let Some(found) = found {
380            return Ok(Some(found));
381        }
382    }
383}
384
385fn validate_streamed_hash(
386    header: &PacketHeader,
387    header_bytes: &[u8; HEADER_SIZE],
388    body: &[u8],
389    offset: u64,
390) -> Result<()> {
391    let mut hasher = Md5State::new();
392    hasher.update(&header_bytes[32..HEADER_SIZE]);
393    hasher.update(body);
394    let computed = hasher.finalize();
395    if computed != header.packet_hash {
396        return Err(Par2Error::PacketHashMismatch { offset });
397    }
398    Ok(())
399}
400
401fn validate_streamed_packet_from_reader(
402    reader: &mut BufReader<File>,
403    header: &PacketHeader,
404    header_bytes: &[u8; HEADER_SIZE],
405    body_len: usize,
406    offset: u64,
407    budget: &PacketScanBudget,
408) -> Result<()> {
409    let mut hasher = Md5State::new();
410    hasher.update(&header_bytes[32..HEADER_SIZE]);
411
412    let mut remaining = body_len;
413    let mut buf = [0u8; 64 * 1024];
414    while remaining > 0 {
415        budget.check_cancelled()?;
416        let take = remaining.min(buf.len());
417        reader.read_exact(&mut buf[..take]).map_err(Par2Error::Io)?;
418        hasher.update(&buf[..take]);
419        remaining -= take;
420    }
421
422    let computed = hasher.finalize();
423    if computed != header.packet_hash {
424        return Err(Par2Error::PacketHashMismatch { offset });
425    }
426    Ok(())
427}
428
429fn read_exact_cancellable(
430    reader: &mut BufReader<File>,
431    destination: &mut [u8],
432    budget: &PacketScanBudget,
433) -> Result<()> {
434    let mut offset = 0usize;
435    while offset < destination.len() {
436        budget.check_cancelled()?;
437        let take = (destination.len() - offset).min(64 * 1024);
438        reader
439            .read_exact(&mut destination[offset..offset + take])
440            .map_err(Par2Error::Io)?;
441        offset += take;
442    }
443    Ok(())
444}
445
446fn max_buffered_non_recovery_body_len(packet_type: PacketType) -> Option<usize> {
447    match packet_type {
448        PacketType::Main => Some(MAX_MAIN_BODY_BYTES),
449        PacketType::FileDescription => Some(MAX_FILE_DESC_BODY_BYTES),
450        PacketType::InputFileSliceChecksum => Some(MAX_IFSC_BODY_BYTES),
451        PacketType::Creator => Some(MAX_CREATOR_BODY_BYTES),
452        PacketType::RecoverySlice | PacketType::Unknown(_) => None,
453    }
454}
455
456fn parse_non_recovery_packet_from_reader(
457    reader: &mut BufReader<File>,
458    header: &PacketHeader,
459    header_bytes: &[u8; HEADER_SIZE],
460    offset: u64,
461    budget: &PacketScanBudget,
462) -> Result<Option<Packet>> {
463    let body_len =
464        usize::try_from(header.body_length()).map_err(|_| Par2Error::ResourceLimitExceeded {
465            reason: format!(
466                "packet body length {} exceeds addressable memory",
467                header.body_length()
468            ),
469        })?;
470    let Some(max_body_len) = max_buffered_non_recovery_body_len(header.packet_type) else {
471        validate_streamed_packet_from_reader(
472            reader,
473            header,
474            header_bytes,
475            body_len,
476            offset,
477            budget,
478        )?;
479        return Ok(None);
480    };
481    if body_len > max_body_len {
482        validate_streamed_packet_from_reader(
483            reader,
484            header,
485            header_bytes,
486            body_len,
487            offset,
488            budget,
489        )?;
490        return Ok(None);
491    }
492
493    let mut body = vec![0u8; body_len];
494    read_exact_cancellable(reader, &mut body, budget)?;
495    validate_streamed_hash(header, header_bytes, &body, offset)?;
496    parse_packet_body(header, body).map(Some).or(Ok(None))
497}
498
499fn parse_recovery_packet_from_reader(
500    reader: &mut BufReader<File>,
501    header: &PacketHeader,
502    offset: u64,
503    path: &Arc<Path>,
504    budget: &PacketScanBudget,
505) -> Result<Packet> {
506    let body_len =
507        usize::try_from(header.body_length()).map_err(|_| Par2Error::ResourceLimitExceeded {
508            reason: format!(
509                "packet body length {} exceeds addressable memory",
510                header.body_length()
511            ),
512        })?;
513    if body_len <= 4 {
514        return Err(Par2Error::InvalidRecoveryPacket {
515            reason: format!("body too short: {body_len} bytes, need more than 4"),
516        });
517    }
518
519    let mut exponent_bytes = [0u8; 4];
520    read_exact_cancellable(reader, &mut exponent_bytes, budget)?;
521    let exponent = u32::from_le_bytes(exponent_bytes);
522    let payload_len = body_len - 4;
523    let payload_offset = offset + HEADER_SIZE as u64 + 4;
524    reader
525        .seek(SeekFrom::Start(payload_offset + payload_len as u64))
526        .map_err(Par2Error::Io)?;
527
528    Ok(Packet::RecoverySlice(RecoverySlicePacket {
529        exponent,
530        // The streaming scanner seeks past recovery payloads without hashing
531        // them, so keep the packet hash around for lazy validation at repair
532        // time (damaged .vol files are routine on Usenet).
533        data: RecoverySliceData::file_backed_shared(
534            Arc::clone(path),
535            payload_offset,
536            payload_len,
537            Some(header.packet_hash),
538        ),
539    }))
540}
541
542/// Collect every packet of an on-disk PAR2 file under the default limits.
543///
544/// Prefer [`scan_packets_from_path_bounded`] when the packets are going to be
545/// deduplicated anyway: this variant retains the raw stream, duplicates
546/// included, and so meters the physical packet count rather than the logical
547/// inventory.
548pub fn scan_packets_from_path_with_set_ids(path: &Path) -> Result<Vec<ScannedPacket>> {
549    scan_packets_from_path_with_set_ids_limited(path, PacketScanLimits::default())
550}
551
552/// [`scan_packets_from_path_with_set_ids`] under caller-chosen limits.
553pub fn scan_packets_from_path_with_set_ids_limited(
554    path: &Path,
555    limits: PacketScanLimits,
556) -> Result<Vec<ScannedPacket>> {
557    let budget = PacketScanBudget::new(limits);
558    collect_packets_from_path(path, &budget)
559}
560
561pub(crate) fn scan_packets_from_path_with_set_ids_cancellable(
562    path: &Path,
563    limits: PacketScanLimits,
564    cancellation: &CancellationToken,
565) -> Result<Vec<ScannedPacket>> {
566    let budget = PacketScanBudget::with_cancellation(limits, Some(cancellation.clone()));
567    collect_packets_from_path(path, &budget)
568}
569
570fn collect_packets_from_path(path: &Path, budget: &PacketScanBudget) -> Result<Vec<ScannedPacket>> {
571    let mut sink = CollectingSink::new(budget);
572    scan_packets_from_path_bounded(path, budget, &mut sink)?;
573    Ok(sink.packets)
574}
575
576/// Stream the packets of an on-disk PAR2 file into `sink` under `budget`.
577///
578/// The scanner holds one packet at a time. Recovery payloads are never read:
579/// each recovery packet is recorded as a file-backed span into `path`, and all
580/// of them share a single interned `Arc<Path>` so a file holding tens of
581/// thousands of recovery packets costs one path allocation rather than one per
582/// packet. Oversized known packets and unknown packets are hash-validated and
583/// discarded without being buffered.
584pub fn scan_packets_from_path_bounded(
585    path: &Path,
586    budget: &PacketScanBudget,
587    sink: &mut dyn PacketSink,
588) -> Result<()> {
589    let file = File::open(path).map_err(Par2Error::Io)?;
590    let file_len = file.metadata().map_err(Par2Error::Io)?.len();
591    crate::file_cache::advise_sequential(&file, path, file_len);
592    let mut reader = BufReader::with_capacity(256 * 1024, file);
593    let shared_path: Arc<Path> = Arc::from(path);
594    let mut interned_path_charged = false;
595    let mut offset = 0u64;
596
597    while let Some(packet_offset) = find_next_magic_in_reader(&mut reader, &mut offset, budget)? {
598        budget.check_cancelled()?;
599        let mut header_bytes = [0u8; HEADER_SIZE];
600        header_bytes[..MAGIC.len()].copy_from_slice(MAGIC);
601
602        match read_exact_cancellable(&mut reader, &mut header_bytes[MAGIC.len()..], budget) {
603            Ok(()) => {}
604            Err(Par2Error::Io(error)) if error.kind() == io::ErrorKind::UnexpectedEof => break,
605            Err(error) => return Err(error),
606        }
607
608        let header = match PacketHeader::parse(&header_bytes, packet_offset) {
609            Ok(header) => header,
610            Err(_) => {
611                reader
612                    .seek(SeekFrom::Start(packet_offset + 1))
613                    .map_err(Par2Error::Io)?;
614                offset = packet_offset + 1;
615                continue;
616            }
617        };
618        if packet_offset
619            .checked_add(header.length)
620            .is_none_or(|packet_end| packet_end > file_len)
621        {
622            reader
623                .seek(SeekFrom::Start(packet_offset + 1))
624                .map_err(Par2Error::Io)?;
625            offset = packet_offset + 1;
626            continue;
627        }
628
629        if matches!(header.packet_type, PacketType::RecoverySlice) && !interned_path_charged {
630            budget.charge_bytes(budget::interned_path_bytes(path))?;
631            interned_path_charged = true;
632        }
633
634        let packet = match header.packet_type {
635            PacketType::RecoverySlice => parse_recovery_packet_from_reader(
636                &mut reader,
637                &header,
638                packet_offset,
639                &shared_path,
640                budget,
641            )
642            .map(Some),
643            _ => parse_non_recovery_packet_from_reader(
644                &mut reader,
645                &header,
646                &header_bytes,
647                packet_offset,
648                budget,
649            ),
650        };
651
652        match packet {
653            Ok(packet) => {
654                // Charged even when the packet is discarded here: an unknown or
655                // oversized packet still cost a full hash pass, and a stream of
656                // nothing but those must stay bounded.
657                budget.charge_examined()?;
658                if let Some(packet) = packet {
659                    sink.accept(packet, packet_offset, header.recovery_set_id)?;
660                }
661                offset = packet_offset + header.length;
662            }
663            Err(Par2Error::Cancelled) => return Err(Par2Error::Cancelled),
664            Err(error @ Par2Error::ResourceLimitExceeded { .. }) => return Err(error),
665            Err(_) => {
666                reader
667                    .seek(SeekFrom::Start(packet_offset + 1))
668                    .map_err(Par2Error::Io)?;
669                offset = packet_offset + 1;
670            }
671        }
672    }
673
674    crate::file_cache::drop_touched_file_cache(
675        reader.get_ref(),
676        path,
677        file_len,
678        0,
679        offset.min(file_len),
680    );
681    Ok(())
682}
683
684pub fn scan_packets_from_path(path: &Path) -> Result<Vec<(Packet, u64)>> {
685    scan_packets_from_path_with_set_ids(path).map(|packets| {
686        packets
687            .into_iter()
688            .map(|packet| (packet.packet, packet.offset))
689            .collect()
690    })
691}
692
693/// Find the byte offset of the next PAR2 magic sequence in `data`.
694fn find_next_magic(data: &[u8]) -> Option<usize> {
695    if data.len() < MAGIC.len() {
696        return None;
697    }
698    for i in 0..=data.len() - MAGIC.len() {
699        if &data[i..i + MAGIC.len()] == MAGIC {
700            return Some(i);
701        }
702    }
703    None
704}
705
706/// Extract the recovery set ID from the first Main packet found in the data.
707pub fn find_recovery_set_id(packets: &[(Packet, u64)]) -> Option<RecoverySetId> {
708    for (packet, _) in packets {
709        if let Packet::Main(main) = packet {
710            return Some(main.recovery_set_id);
711        }
712    }
713    None
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719    use md5::{Digest, Md5};
720    use std::io::Write;
721    use tempfile::NamedTempFile;
722
723    /// Helper to build a complete valid packet (header + body).
724    fn make_full_packet(packet_type: &[u8; 16], body: &[u8], recovery_set_id: [u8; 16]) -> Vec<u8> {
725        let length = (HEADER_SIZE + body.len()) as u64;
726
727        // Build bytes 32..length for hashing
728        let mut hash_input = Vec::new();
729        hash_input.extend_from_slice(&recovery_set_id);
730        hash_input.extend_from_slice(packet_type);
731        hash_input.extend_from_slice(body);
732
733        let packet_hash: [u8; 16] = Md5::digest(&hash_input).into();
734
735        let mut data = Vec::new();
736        data.extend_from_slice(MAGIC);
737        data.extend_from_slice(&length.to_le_bytes());
738        data.extend_from_slice(&packet_hash);
739        data.extend_from_slice(&recovery_set_id);
740        data.extend_from_slice(packet_type);
741        data.extend_from_slice(body);
742        data
743    }
744
745    fn make_creator_packet(creator: &str, rsid: [u8; 16]) -> Vec<u8> {
746        // Pad creator to multiple of 4 for body alignment
747        let mut body = creator.as_bytes().to_vec();
748        while !body.len().is_multiple_of(4) {
749            body.push(0);
750        }
751        make_full_packet(header::TYPE_CREATOR, &body, rsid)
752    }
753
754    fn make_main_packet_bytes(slice_size: u64, rsid: [u8; 16]) -> Vec<u8> {
755        let mut body = Vec::new();
756        body.extend_from_slice(&slice_size.to_le_bytes());
757        body.extend_from_slice(&0u32.to_le_bytes()); // 0 recovery file IDs
758        make_full_packet(header::TYPE_MAIN, &body, rsid)
759    }
760
761    #[test]
762    fn parse_creator_packet() {
763        let rsid = [0x42; 16];
764        let data = make_creator_packet("TestCreator", rsid);
765        let (packet, consumed) = parse_packet(&data, 0).unwrap();
766        assert_eq!(consumed, data.len());
767        match packet {
768            Packet::Creator(c) => assert_eq!(c.creator_id, "TestCreator"),
769            other => panic!("expected Creator, got {other:?}"),
770        }
771    }
772
773    #[test]
774    fn scan_multiple_packets() {
775        let rsid = [0x11; 16];
776        let mut stream = Vec::new();
777        stream.extend_from_slice(&make_creator_packet("App1", rsid));
778        stream.extend_from_slice(&make_main_packet_bytes(4096, rsid));
779        stream.extend_from_slice(&make_creator_packet("App2", rsid));
780
781        let packets = scan_packets(&stream, 0).unwrap();
782        assert_eq!(packets.len(), 3);
783        assert!(matches!(&packets[0].0, Packet::Creator(_)));
784        assert!(matches!(&packets[1].0, Packet::Main(_)));
785        assert!(matches!(&packets[2].0, Packet::Creator(_)));
786    }
787
788    #[test]
789    fn scan_skips_garbage() {
790        let rsid = [0x22; 16];
791        let mut stream = Vec::new();
792        // Some garbage bytes before the first packet
793        stream.extend_from_slice(&[0xFF; 37]);
794        stream.extend_from_slice(&make_creator_packet("Found", rsid));
795
796        let packets = scan_packets(&stream, 0).unwrap();
797        assert_eq!(packets.len(), 1);
798        assert_eq!(packets[0].1, 37); // offset should be 37
799        match &packets[0].0 {
800            Packet::Creator(c) => assert_eq!(c.creator_id, "Found"),
801            other => panic!("expected Creator, got {other:?}"),
802        }
803    }
804
805    #[test]
806    fn scan_handles_garbage_between_packets() {
807        let rsid = [0x33; 16];
808        let mut stream = Vec::new();
809        stream.extend_from_slice(&make_creator_packet("First", rsid));
810        // Garbage between packets
811        stream.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
812        stream.extend_from_slice(&make_creator_packet("Second", rsid));
813
814        let packets = scan_packets(&stream, 100).unwrap();
815        assert_eq!(packets.len(), 2);
816    }
817
818    #[test]
819    fn scan_empty_data() {
820        let packets = scan_packets(&[], 0).unwrap();
821        assert!(packets.is_empty());
822    }
823
824    #[test]
825    fn scan_short_data() {
826        let packets = scan_packets(&[0u8; 10], 0).unwrap();
827        assert!(packets.is_empty());
828    }
829
830    #[test]
831    fn find_next_magic_works() {
832        let mut data = vec![0u8; 20];
833        data.extend_from_slice(MAGIC);
834        data.extend_from_slice(&[0u8; 10]);
835        assert_eq!(find_next_magic(&data), Some(20));
836    }
837
838    #[test]
839    fn find_next_magic_at_start() {
840        let mut data = Vec::new();
841        data.extend_from_slice(MAGIC);
842        assert_eq!(find_next_magic(&data), Some(0));
843    }
844
845    #[test]
846    fn find_next_magic_not_found() {
847        let data = [0u8; 100];
848        assert_eq!(find_next_magic(&data), None);
849    }
850
851    #[test]
852    fn parse_packet_hash_mismatch() {
853        let rsid = [0; 16];
854        let mut data = make_creator_packet("test", rsid);
855        // Corrupt a body byte
856        let last = data.len() - 1;
857        data[last] ^= 0x01;
858        let err = parse_packet(&data, 5).unwrap_err();
859        assert!(matches!(err, Par2Error::PacketHashMismatch { offset: 5 }));
860    }
861
862    #[test]
863    fn parse_unknown_packet_type() {
864        let custom_type = b"PAR 2.0\x00TestType";
865        let body = [0u8; 16]; // 16 bytes body
866        let rsid = [0; 16];
867        let data = make_full_packet(custom_type, &body, rsid);
868
869        let (packet, _) = parse_packet(&data, 0).unwrap();
870        match packet {
871            Packet::Unknown {
872                packet_type,
873                body: b,
874            } => {
875                assert_eq!(packet_type, *custom_type);
876                assert_eq!(b.len(), 16);
877            }
878            other => panic!("expected Unknown, got {other:?}"),
879        }
880    }
881
882    #[test]
883    fn path_scanner_streams_and_ignores_large_unknown_packets() {
884        let custom_type = b"PAR 2.0\x00TestType";
885        let rsid = [0x55; 16];
886        let unknown_body = vec![0xA5; 1024 * 1024 + 4];
887        let mut stream = make_full_packet(custom_type, &unknown_body, rsid);
888        stream.extend_from_slice(&make_main_packet_bytes(4096, rsid));
889
890        let mut file = NamedTempFile::new().unwrap();
891        file.write_all(&stream).unwrap();
892
893        let packets = scan_packets_from_path(file.path()).unwrap();
894        assert_eq!(packets.len(), 1);
895        assert!(matches!(&packets[0].0, Packet::Main(_)));
896    }
897
898    #[test]
899    fn path_scanner_walks_large_valid_packet_inventory() {
900        let rsid = [0x5A; 16];
901        let creator = make_creator_packet("stress", rsid);
902        let mut stream = Vec::with_capacity(creator.len() * 70_000 + HEADER_SIZE + 12);
903        for _ in 0..70_000 {
904            stream.extend_from_slice(&creator);
905        }
906        stream.extend_from_slice(&make_main_packet_bytes(4096, rsid));
907
908        let mut file = NamedTempFile::new().unwrap();
909        file.write_all(&stream).unwrap();
910
911        let packets = scan_packets_from_path(file.path()).unwrap();
912        assert_eq!(packets.len(), 70_001);
913        assert!(matches!(&packets[70_000].0, Packet::Main(_)));
914    }
915
916    #[test]
917    fn path_scanner_skips_valid_hash_oversized_known_packets_by_boundary() {
918        let rsid = [0x66; 16];
919        let embedded_rsid = [0x99; 16];
920        let embedded_main = make_main_packet_bytes(8192, embedded_rsid);
921        let mut oversized_creator_body = vec![0u8; MAX_CREATOR_BODY_BYTES + 4];
922        oversized_creator_body[..embedded_main.len()].copy_from_slice(&embedded_main);
923
924        let mut stream = make_full_packet(header::TYPE_CREATOR, &oversized_creator_body, rsid);
925        stream.extend_from_slice(&make_main_packet_bytes(4096, rsid));
926
927        let mut file = NamedTempFile::new().unwrap();
928        file.write_all(&stream).unwrap();
929
930        let packets = scan_packets_from_path(file.path()).unwrap();
931        assert_eq!(packets.len(), 1);
932        match &packets[0].0 {
933            Packet::Main(main) => assert_eq!(*main.recovery_set_id.as_bytes(), rsid),
934            other => panic!("expected Main, got {other:?}"),
935        }
936    }
937
938    #[test]
939    fn find_recovery_set_id_works() {
940        let rsid = [0x77; 16];
941        let stream = make_main_packet_bytes(1024, rsid);
942        let packets = scan_packets(&stream, 0).unwrap();
943        let found = find_recovery_set_id(&packets).unwrap();
944        assert_eq!(*found.as_bytes(), rsid);
945    }
946
947    #[test]
948    fn find_recovery_set_id_none() {
949        let rsid = [0; 16];
950        let stream = make_creator_packet("test", rsid);
951        let packets = scan_packets(&stream, 0).unwrap();
952        assert!(find_recovery_set_id(&packets).is_none());
953    }
954
955    fn make_recovery_packet(exponent: u32, payload: &[u8], rsid: [u8; 16]) -> Vec<u8> {
956        let mut body = Vec::with_capacity(4 + payload.len());
957        body.extend_from_slice(&exponent.to_le_bytes());
958        body.extend_from_slice(payload);
959        make_full_packet(header::TYPE_RECOVERY, &body, rsid)
960    }
961
962    /// `count` distinct creator packets, so nothing collapses under dedup.
963    fn creator_run(count: usize, rsid: [u8; 16]) -> Vec<u8> {
964        let mut stream = Vec::new();
965        for i in 0..count {
966            stream.extend_from_slice(&make_creator_packet(&format!("app-{i}"), rsid));
967        }
968        stream
969    }
970
971    #[test]
972    fn in_memory_scan_refuses_one_packet_past_the_configured_limit() {
973        let rsid = [0x81; 16];
974        let limits = PacketScanLimits::default()
975            .with_max_retained_packets(8)
976            .with_max_examined_packets(8);
977
978        let at_limit = creator_run(8, rsid);
979        assert_eq!(
980            scan_packets_with_limits(&at_limit, 0, limits)
981                .unwrap()
982                .len(),
983            8
984        );
985
986        let over_limit = creator_run(9, rsid);
987        let error = scan_packets_with_limits(&over_limit, 0, limits).unwrap_err();
988        assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
989    }
990
991    #[test]
992    fn disk_scan_refuses_one_packet_past_the_configured_limit() {
993        let rsid = [0x82; 16];
994        let limits = PacketScanLimits::default()
995            .with_max_retained_packets(8)
996            .with_max_examined_packets(8);
997
998        let mut at_limit = NamedTempFile::new().unwrap();
999        at_limit.write_all(&creator_run(8, rsid)).unwrap();
1000        assert_eq!(
1001            scan_packets_from_path_with_set_ids_limited(at_limit.path(), limits)
1002                .unwrap()
1003                .len(),
1004            8
1005        );
1006
1007        let mut over_limit = NamedTempFile::new().unwrap();
1008        over_limit.write_all(&creator_run(9, rsid)).unwrap();
1009        let error =
1010            scan_packets_from_path_with_set_ids_limited(over_limit.path(), limits).unwrap_err();
1011        assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
1012    }
1013
1014    /// Exhaustion must be distinguishable from "this file held nothing". A
1015    /// caller that saw an empty vector would carry on with a set that quietly
1016    /// lost recovery data.
1017    #[test]
1018    fn limit_exhaustion_never_looks_like_an_empty_scan() {
1019        let rsid = [0x83; 16];
1020        let limits = PacketScanLimits::default().with_max_retained_packets(1);
1021        let stream = creator_run(4, rsid);
1022
1023        let mut file = NamedTempFile::new().unwrap();
1024        file.write_all(&stream).unwrap();
1025
1026        assert!(matches!(
1027            scan_packets_with_limits(&stream, 0, limits),
1028            Err(Par2Error::ResourceLimitExceeded { .. })
1029        ));
1030        assert!(matches!(
1031            scan_packets_from_path_with_set_ids_limited(file.path(), limits),
1032            Err(Par2Error::ResourceLimitExceeded { .. })
1033        ));
1034    }
1035
1036    #[test]
1037    fn the_byte_meter_alone_can_refuse_a_scan() {
1038        let rsid = [0x84; 16];
1039        let stream = creator_run(64, rsid);
1040        let limits = PacketScanLimits::default().with_max_retained_metadata_bytes(256);
1041        let error = scan_packets_with_limits(&stream, 0, limits).unwrap_err();
1042        assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
1043    }
1044
1045    #[test]
1046    fn duplicate_packets_spend_work_budget_but_not_retention_budget() {
1047        // The collecting scanners keep every packet, so the raw stream is what
1048        // they meter; the deduplicating sinks are covered where they live.
1049        let rsid = [0x85; 16];
1050        let identical = make_creator_packet("same", rsid);
1051        let mut stream = Vec::new();
1052        for _ in 0..16 {
1053            stream.extend_from_slice(&identical);
1054        }
1055
1056        let budget = PacketScanBudget::new(PacketScanLimits::default());
1057        let mut kept = 0usize;
1058        let mut sink = |_packet: Packet, _offset: u64, _set: RecoverySetId| -> Result<()> {
1059            // A deduplicating sink retains only the first of the run.
1060            if kept == 0 {
1061                kept += 1;
1062                budget.charge_retained(64)?;
1063            }
1064            Ok(())
1065        };
1066        scan_packets_bounded(&stream, 0, &budget, &mut sink).unwrap();
1067
1068        assert_eq!(budget.examined(), 16, "every duplicate is work");
1069        assert_eq!(budget.retained_packets(), 1, "only one is retention");
1070    }
1071
1072    #[test]
1073    fn scanning_stops_on_cancellation_rather_than_running_to_the_end() {
1074        let rsid = [0x86; 16];
1075        let stream = creator_run(64, rsid);
1076        let mut file = NamedTempFile::new().unwrap();
1077        file.write_all(&stream).unwrap();
1078
1079        let cancel = CancellationToken::new();
1080        cancel.cancel();
1081        let budget =
1082            PacketScanBudget::with_cancellation(PacketScanLimits::default(), Some(cancel.clone()));
1083        let mut sink = CollectingSink::new(&budget);
1084        assert!(matches!(
1085            scan_packets_from_path_bounded(file.path(), &budget, &mut sink),
1086            Err(Par2Error::Cancelled)
1087        ));
1088
1089        let budget = PacketScanBudget::with_cancellation(PacketScanLimits::default(), Some(cancel));
1090        let mut sink = CollectingSink::new(&budget);
1091        assert!(matches!(
1092            scan_packets_bounded(&stream, 0, &budget, &mut sink),
1093            Err(Par2Error::Cancelled)
1094        ));
1095    }
1096
1097    /// Cancellation asserted partway through: the scan must stop, and it must
1098    /// not hand back the packets it had already collected.
1099    #[test]
1100    fn cancellation_mid_scan_aborts_instead_of_truncating() {
1101        let rsid = [0x87; 16];
1102        let stream = creator_run(64, rsid);
1103        let cancel = CancellationToken::new();
1104        let budget =
1105            PacketScanBudget::with_cancellation(PacketScanLimits::default(), Some(cancel.clone()));
1106
1107        let mut seen = 0usize;
1108        let mut sink = |_packet: Packet, _offset: u64, _set: RecoverySetId| -> Result<()> {
1109            seen += 1;
1110            if seen == 4 {
1111                cancel.cancel();
1112            }
1113            Ok(())
1114        };
1115        let error = scan_packets_bounded(&stream, 0, &budget, &mut sink).unwrap_err();
1116        assert!(matches!(error, Par2Error::Cancelled));
1117        assert!(seen < 64, "the scan stopped early, seen={seen}");
1118    }
1119
1120    #[test]
1121    fn in_memory_scan_drops_unknown_packet_bodies() {
1122        let custom_type = b"PAR 2.0\x00TestType";
1123        let rsid = [0x88; 16];
1124        let mut stream = make_full_packet(custom_type, &vec![0xA5; 4096], rsid);
1125        stream.extend_from_slice(&make_main_packet_bytes(4096, rsid));
1126
1127        let packets = scan_packets(&stream, 0).unwrap();
1128        assert_eq!(packets.len(), 1, "the unknown packet is not delivered");
1129        assert!(matches!(&packets[0].0, Packet::Main(_)));
1130    }
1131
1132    /// Unknown packets still cost the examined meter: a stream of nothing but
1133    /// unknown packets must not be able to run unbounded just because none of
1134    /// them is retained.
1135    #[test]
1136    fn unknown_packets_still_spend_the_examined_meter() {
1137        let custom_type = b"PAR 2.0\x00TestType";
1138        let rsid = [0x89; 16];
1139        let mut stream = Vec::new();
1140        for i in 0..12u8 {
1141            stream.extend_from_slice(&make_full_packet(custom_type, &[i; 16], rsid));
1142        }
1143        let mut file = NamedTempFile::new().unwrap();
1144        file.write_all(&stream).unwrap();
1145
1146        let limits = PacketScanLimits::default().with_max_examined_packets(8);
1147        assert!(matches!(
1148            scan_packets_with_limits(&stream, 0, limits),
1149            Err(Par2Error::ResourceLimitExceeded { .. })
1150        ));
1151        assert!(matches!(
1152            scan_packets_from_path_with_set_ids_limited(file.path(), limits),
1153            Err(Par2Error::ResourceLimitExceeded { .. })
1154        ));
1155    }
1156
1157    #[test]
1158    fn every_recovery_packet_in_a_volume_shares_one_interned_path() {
1159        let rsid = [0x8A; 16];
1160        let mut stream = make_main_packet_bytes(4, rsid);
1161        for exponent in 0..64u32 {
1162            stream.extend_from_slice(&make_recovery_packet(exponent, &[0xAB; 4], rsid));
1163        }
1164        let mut file = NamedTempFile::new().unwrap();
1165        file.write_all(&stream).unwrap();
1166
1167        let packets = scan_packets_from_path_with_set_ids(file.path()).unwrap();
1168        let paths: Vec<Arc<Path>> = packets
1169            .iter()
1170            .filter_map(|scanned| match &scanned.packet {
1171                Packet::RecoverySlice(slice) => match &slice.data {
1172                    RecoverySliceData::FileBacked { path, .. } => Some(Arc::clone(path)),
1173                    RecoverySliceData::InMemory(_) => None,
1174                },
1175                _ => None,
1176            })
1177            .collect();
1178
1179        assert_eq!(paths.len(), 64);
1180        for path in &paths[1..] {
1181            assert!(
1182                Arc::ptr_eq(&paths[0], path),
1183                "recovery packets must share one allocation for the volume path"
1184            );
1185        }
1186    }
1187
1188    /// The streaming scanner never hashes recovery payloads, so it records the
1189    /// packet hash for later. That deferred validation has to still work
1190    /// against the interned path.
1191    #[test]
1192    fn file_backed_recovery_payloads_still_validate_their_packet_hash() {
1193        let rsid = [0x8B; 16];
1194        let mut stream = make_main_packet_bytes(8, rsid);
1195        stream.extend_from_slice(&make_recovery_packet(7, &[0xC3; 8], rsid));
1196        let mut file = NamedTempFile::new().unwrap();
1197        file.write_all(&stream).unwrap();
1198
1199        let packets = scan_packets_from_path(file.path()).unwrap();
1200        let Packet::RecoverySlice(slice) = &packets[1].0 else {
1201            panic!("expected a recovery packet, got {:?}", packets[1].0);
1202        };
1203        assert!(slice.data.as_bytes().is_none(), "payload stays file-backed");
1204        assert_eq!(slice.data.to_vec().unwrap(), vec![0xC3; 8]);
1205        assert!(slice.data.validate_packet_hash(&rsid, 7).unwrap());
1206
1207        // Corrupt the payload on disk and the same check must now fail.
1208        let mut corrupted = stream.clone();
1209        let last = corrupted.len() - 1;
1210        corrupted[last] ^= 0xFF;
1211        std::fs::write(file.path(), &corrupted).unwrap();
1212        assert!(!slice.data.validate_packet_hash(&rsid, 7).unwrap());
1213    }
1214}