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#[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
60pub 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
89struct 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 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
129fn 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 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
229pub fn scan_packets(data: &[u8], base_offset: u64) -> Result<Vec<(Packet, u64)>> {
244 scan_packets_with_limits(data, base_offset, PacketScanLimits::default())
245}
246
247pub 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
263pub 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 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 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 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 break;
326 }
327 }
328 }
329 }
330 }
331
332 Ok(())
333}
334
335fn 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 data: RecoverySliceData::file_backed_shared(
534 Arc::clone(path),
535 payload_offset,
536 payload_len,
537 Some(header.packet_hash),
538 ),
539 }))
540}
541
542pub 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
552pub 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
576pub 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 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
693fn 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
706pub 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 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 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 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()); 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 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); 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 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 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]; 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 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 #[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 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 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 #[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 #[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 #[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 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}