1use std::collections::HashSet;
2use std::fs::{self, File, Metadata};
3use std::io::{self, Read, Seek, SeekFrom};
4use std::path::{Component, Path, PathBuf};
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use rayon::prelude::*;
8
9use crate::checksum::{self, FileHashState, Md5State, SliceChecksumState};
10use crate::error::{Par2Error, Result};
11use crate::md5_simd;
12use crate::path::translate_par2_name_to_relative;
13use crate::types::{
14 CancellationToken, FileId, MAX_FILES_PER_SET, MAX_SLICES_PER_FILE, MAX_TOTAL_INPUT_SLICES,
15 ProgressCallback, ProgressStage, SliceChecksum,
16};
17
18use super::encode::{ForwardSourceObserver, ForwardSourceProvider};
19
20const FIRST_HASH_BYTES: u64 = 16 * 1024;
21const READ_BUFFER_BYTES: usize = 256 * 1024;
22const MAX_PAR2_NAME_BYTES: usize = 100_000;
23pub(crate) const CREATE_MD5_BATCH_MEMORY_BYTES: usize = 4 * 1024 * 1024;
29
30pub(crate) fn create_md5_batch_lanes(block_size: usize) -> usize {
38 if block_size == 0 {
39 return 1;
40 }
41 (CREATE_MD5_BATCH_MEMORY_BYTES / block_size).clamp(1, md5_simd::max_lanes())
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct CreationSource {
68 pub path: PathBuf,
70 pub par2_name: String,
72 pub file_id: FileId,
74 pub file_length: u64,
76 pub hash_full: [u8; 16],
79 pub hash_16k: [u8; 16],
81 pub slice_checksums: Vec<SliceChecksum>,
84}
85
86const DEFERRED_SLICE_CHECKSUM: SliceChecksum = SliceChecksum {
88 crc32: 0,
89 md5: [0; 16],
90};
91
92impl CreationSource {
93 pub fn slice_count(&self) -> u32 {
95 self.slice_checksums.len() as u32
96 }
97}
98
99#[derive(Debug, Clone)]
100pub(crate) struct InputLength {
101 pub length: u64,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105struct SourceFingerprint {
106 length: u64,
107 modified: Option<std::time::SystemTime>,
108 #[cfg(unix)]
109 device: u64,
110 #[cfg(unix)]
111 inode: u64,
112}
113
114pub(crate) struct SourceScanCache {
131 entries: std::sync::Mutex<Vec<CachedScan>>,
132}
133
134struct CachedScan {
135 fingerprint: SourceFingerprint,
136 block_size: u64,
137 source: CreationSource,
138}
139
140impl SourceScanCache {
141 pub(crate) fn new() -> Self {
142 Self {
143 entries: std::sync::Mutex::new(Vec::new()),
144 }
145 }
146
147 fn take(
148 &self,
149 path: &Path,
150 fingerprint: &SourceFingerprint,
151 block_size: u64,
152 ) -> Option<CreationSource> {
153 let mut entries = self
154 .entries
155 .lock()
156 .unwrap_or_else(std::sync::PoisonError::into_inner);
157 let index = entries.iter().position(|entry| {
158 entry.block_size == block_size
159 && entry.fingerprint == *fingerprint
160 && entry.source.path == path
161 })?;
162 Some(entries.swap_remove(index).source)
163 }
164
165 fn store(&self, fingerprint: SourceFingerprint, block_size: u64, source: &CreationSource) {
166 let mut entries = self
167 .entries
168 .lock()
169 .unwrap_or_else(std::sync::PoisonError::into_inner);
170 entries.retain(|entry| entry.source.path != source.path);
171 entries.push(CachedScan {
172 fingerprint,
173 block_size,
174 source: source.clone(),
175 });
176 }
177}
178
179impl SourceFingerprint {
180 fn from_metadata(metadata: &Metadata) -> Self {
181 #[cfg(unix)]
182 use std::os::unix::fs::MetadataExt;
183
184 Self {
185 length: metadata.len(),
186 modified: metadata.modified().ok(),
187 #[cfg(unix)]
188 device: metadata.dev(),
189 #[cfg(unix)]
190 inode: metadata.ino(),
191 }
192 }
193}
194
195pub(crate) struct CollectedSources {
205 pub(crate) sources: Vec<CreationSource>,
206 pub(crate) skipped_empty: Vec<PathBuf>,
210}
211
212pub(crate) fn collect_sources(
219 base_path: &Path,
220 inputs: &[PathBuf],
221 block_size: u64,
222 cancellation: &CancellationToken,
223 progress: Option<&ProgressCallback>,
224 total_bytes: u64,
225 cache: Option<&SourceScanCache>,
226) -> Result<CollectedSources> {
227 if inputs.is_empty() {
228 return Err(Par2Error::InvalidCreationOptions {
229 reason: "at least one source file is required".to_string(),
230 });
231 }
232 let base = fs::canonicalize(base_path).map_err(Par2Error::Io)?;
233 let metadata = fs::metadata(&base).map_err(Par2Error::Io)?;
234 if !metadata.is_dir() {
235 return Err(Par2Error::UnsafeCreationSource {
236 path: base.display().to_string(),
237 reason: "base path is not a directory".to_string(),
238 });
239 }
240
241 let mut active_inputs = Vec::with_capacity(inputs.len());
242 let mut skipped_empty = Vec::new();
243 for input in inputs {
244 if cancellation.is_cancelled() {
245 return Err(Par2Error::Cancelled);
246 }
247 let path = resolve_input_path(&base, input)?;
248 let metadata = fs::metadata(&path).map_err(Par2Error::Io)?;
249 if !metadata.is_file() {
250 return Err(Par2Error::UnsafeCreationSource {
251 path: input.display().to_string(),
252 reason: "source is not a regular file".to_string(),
253 });
254 }
255 let relative = path
256 .strip_prefix(&base)
257 .map_err(|_| Par2Error::UnsafeCreationSource {
258 path: input.display().to_string(),
259 reason: "source is outside the base directory".to_string(),
260 })?;
261 validate_relative_path(relative, input)?;
262 if relative.to_str().is_none() {
263 return Err(Par2Error::UnsafeCreationSource {
264 path: input.display().to_string(),
265 reason: "source filename is not valid UTF-8".to_string(),
266 });
267 }
268 if metadata.len() > 0 {
269 active_inputs.push(input);
270 } else {
271 skipped_empty.push(input.clone());
275 }
276 }
277 if active_inputs.is_empty() {
278 return Err(Par2Error::InvalidCreationOptions {
279 reason: "at least one non-empty source file is required".to_string(),
280 });
281 }
282 validate_main_file_count(active_inputs.len())?;
283
284 let mut names = HashSet::with_capacity(active_inputs.len());
285 let mut ids = HashSet::with_capacity(active_inputs.len());
286 let bytes_processed = AtomicU64::new(0);
287 let file_total =
288 u32::try_from(active_inputs.len()).map_err(|_| Par2Error::ResourceLimitExceeded {
289 reason: "source file count exceeds the supported progress range".to_string(),
290 })?;
291 let mut total_slices = 0usize;
292
293 let hash_one = |(index, input): (usize, &&PathBuf)| -> Result<CreationSource> {
294 if cancellation.is_cancelled() {
295 return Err(Par2Error::Cancelled);
296 }
297 resolve_source_identity(
298 &base,
299 input,
300 block_size,
301 cancellation,
302 progress,
303 index as u32,
304 file_total,
305 &bytes_processed,
306 total_bytes,
307 cache,
308 )
309 };
310 let scan_parallel = reedsolomon_rs::threading::parallel_enabled()
318 && active_inputs.len() > 1
319 && super::encode::configured_create_threads() != 1;
320 let scanned: Vec<CreationSource> = if scan_parallel {
321 active_inputs
328 .par_iter()
329 .enumerate()
330 .map(hash_one)
331 .collect::<Vec<Result<_>>>()
332 .into_iter()
333 .collect::<Result<Vec<_>>>()?
334 } else {
335 active_inputs
336 .iter()
337 .enumerate()
338 .map(hash_one)
339 .collect::<Result<Vec<_>>>()?
340 };
341 let mut sources = Vec::with_capacity(active_inputs.len());
346 sources.extend(scanned);
347
348 for (source, input) in sources.iter().zip(active_inputs.iter()) {
349 if !names.insert(source.par2_name.clone()) {
350 return Err(Par2Error::UnsafeCreationSource {
351 path: input.display().to_string(),
352 reason: "duplicate relative PAR2 name".to_string(),
353 });
354 }
355 if !ids.insert(source.file_id) {
356 return Err(Par2Error::UnsafeCreationSource {
357 path: input.display().to_string(),
358 reason: "duplicate PAR2 file identifier".to_string(),
359 });
360 }
361 total_slices = total_slices
362 .checked_add(source.slice_checksums.len())
363 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
364 reason: "total source slice count overflows".to_string(),
365 })?;
366 if total_slices > MAX_TOTAL_INPUT_SLICES {
367 return Err(Par2Error::ResourceLimitExceeded {
368 reason: format!(
369 "total source slice count {total_slices} exceeds {MAX_TOTAL_INPUT_SLICES}"
370 ),
371 });
372 }
373 }
374 Ok(CollectedSources {
375 sources,
376 skipped_empty,
377 })
378}
379
380pub(crate) fn collect_input_lengths(
382 base_path: &Path,
383 inputs: &[PathBuf],
384 cancellation: &CancellationToken,
385) -> Result<Vec<InputLength>> {
386 if inputs.is_empty() {
387 return Err(Par2Error::InvalidCreationOptions {
388 reason: "at least one source file is required".to_string(),
389 });
390 }
391 let base = fs::canonicalize(base_path).map_err(Par2Error::Io)?;
392 let base_metadata = fs::metadata(&base).map_err(Par2Error::Io)?;
393 if !base_metadata.is_dir() {
394 return Err(Par2Error::UnsafeCreationSource {
395 path: base.display().to_string(),
396 reason: "base path is not a directory".to_string(),
397 });
398 }
399
400 let mut lengths = Vec::with_capacity(inputs.len());
401 for input in inputs {
402 if cancellation.is_cancelled() {
403 return Err(Par2Error::Cancelled);
404 }
405 let path = resolve_input_path(&base, input)?;
406 let metadata = fs::metadata(&path).map_err(Par2Error::Io)?;
407 if !metadata.is_file() {
408 return Err(Par2Error::UnsafeCreationSource {
409 path: input.display().to_string(),
410 reason: "source is not a regular file".to_string(),
411 });
412 }
413 let relative = path
414 .strip_prefix(&base)
415 .map_err(|_| Par2Error::UnsafeCreationSource {
416 path: input.display().to_string(),
417 reason: "source is outside the base directory".to_string(),
418 })?;
419 validate_relative_path(relative, input)?;
420 if relative.to_str().is_none() {
421 return Err(Par2Error::UnsafeCreationSource {
422 path: input.display().to_string(),
423 reason: "source filename is not valid UTF-8".to_string(),
424 });
425 }
426 if metadata.len() > 0 {
427 lengths.push(InputLength {
428 length: metadata.len(),
429 });
430 }
431 }
432 validate_main_file_count(lengths.len())?;
433 Ok(lengths)
434}
435
436fn validate_main_file_count(file_count: usize) -> Result<()> {
437 if file_count > MAX_FILES_PER_SET {
438 return Err(Par2Error::ResourceLimitExceeded {
439 reason: format!(
440 "source file count {file_count} exceeds the Main packet limit of {MAX_FILES_PER_SET}"
441 ),
442 });
443 }
444 Ok(())
445}
446
447pub(crate) struct DiskSourceProvider<'a> {
449 sources: &'a [CreationSource],
450 files: Vec<File>,
451 fingerprints: Vec<SourceFingerprint>,
452 slice_starts: Vec<usize>,
453 slice_size: usize,
454 cancellation: &'a CancellationToken,
455}
456
457impl<'a> DiskSourceProvider<'a> {
458 pub(crate) fn open(
459 sources: &'a [CreationSource],
460 slice_size: usize,
461 cancellation: &'a CancellationToken,
462 ) -> Result<Self> {
463 if slice_size == 0 {
464 return Err(Par2Error::ResourceLimitExceeded {
465 reason: "source provider slice size is zero".to_string(),
466 });
467 }
468 let mut files = Vec::with_capacity(sources.len());
469 let mut fingerprints = Vec::with_capacity(sources.len());
470 let mut slice_starts = Vec::with_capacity(sources.len());
471 let mut next_start = 0usize;
472 for source in sources {
473 if cancellation.is_cancelled() {
474 return Err(Par2Error::Cancelled);
475 }
476 let metadata = fs::metadata(&source.path).map_err(Par2Error::Io)?;
477 if !metadata.is_file() || metadata.len() != source.file_length {
478 return Err(Par2Error::CreationSourceChanged {
479 path: source.path.display().to_string(),
480 });
481 }
482 let slice_count = source.slice_checksums.len();
483 slice_starts.push(next_start);
484 next_start = next_start.checked_add(slice_count).ok_or_else(|| {
485 Par2Error::ResourceLimitExceeded {
486 reason: "source slice provider index overflows".to_string(),
487 }
488 })?;
489 fingerprints.push(SourceFingerprint::from_metadata(&metadata));
490 files.push(File::open(&source.path).map_err(Par2Error::Io)?);
491 }
492 Ok(Self {
493 sources,
494 files,
495 fingerprints,
496 slice_starts,
497 slice_size,
498 cancellation,
499 })
500 }
501
502 pub(crate) fn verify_unchanged(&self) -> Result<()> {
503 for (source, fingerprint) in self.sources.iter().zip(&self.fingerprints) {
504 let metadata = fs::metadata(&source.path).map_err(Par2Error::Io)?;
505 if SourceFingerprint::from_metadata(&metadata) != *fingerprint {
506 return Err(Par2Error::CreationSourceChanged {
507 path: source.path.display().to_string(),
508 });
509 }
510 }
511 Ok(())
512 }
513
514 fn source_location(&self, source_index: usize) -> Result<(usize, usize)> {
515 let file_index = self
516 .slice_starts
517 .partition_point(|&start| start <= source_index)
518 .checked_sub(1)
519 .ok_or_else(|| Par2Error::CreationSourceChanged {
520 path: "source slice index is out of range".to_string(),
521 })?;
522 let local_index = source_index
523 .checked_sub(self.slice_starts[file_index])
524 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
525 reason: "source slice index underflows".to_string(),
526 })?;
527 if local_index >= self.sources[file_index].slice_checksums.len() {
528 return Err(Par2Error::CreationSourceChanged {
529 path: self.sources[file_index].path.display().to_string(),
530 });
531 }
532 Ok((file_index, local_index))
533 }
534}
535
536impl ForwardSourceProvider for DiskSourceProvider<'_> {
537 fn source_count(&self) -> usize {
538 self.sources
539 .iter()
540 .map(|source| source.slice_checksums.len())
541 .sum()
542 }
543
544 fn source_slice_len(&self, source_index: usize) -> Result<usize> {
545 let (file_index, local_index) = self.source_location(source_index)?;
546 let offset = (local_index as u64)
547 .checked_mul(self.slice_size as u64)
548 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
549 reason: "source slice offset overflows".to_string(),
550 })?;
551 usize::try_from(
552 self.sources[file_index]
553 .file_length
554 .saturating_sub(offset)
555 .min(self.slice_size as u64),
556 )
557 .map_err(|_| Par2Error::ResourceLimitExceeded {
558 reason: "source slice length exceeds addressable memory".to_string(),
559 })
560 }
561
562 fn read_source_chunk(
563 &mut self,
564 source_index: usize,
565 offset: usize,
566 destination: &mut [u8],
567 ) -> Result<usize> {
568 if self.cancellation.is_cancelled() {
569 return Err(Par2Error::Cancelled);
570 }
571 let (file_index, local_index) = self.source_location(source_index)?;
572 let slice_len = self.source_slice_len(source_index)?;
573 let start = offset.min(slice_len);
574 let take = destination.len().min(slice_len.saturating_sub(start));
575 if take == 0 {
576 return Ok(0);
577 }
578 let slice_offset = (local_index as u64)
579 .checked_mul(self.slice_size as u64)
580 .and_then(|value| value.checked_add(start as u64))
581 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
582 reason: "source read offset overflows".to_string(),
583 })?;
584 self.files[file_index]
585 .seek(SeekFrom::Start(slice_offset))
586 .map_err(Par2Error::Io)?;
587 self.files[file_index]
588 .read_exact(&mut destination[..take])
589 .map_err(|error| {
590 if error.kind() == io::ErrorKind::UnexpectedEof {
591 Par2Error::CreationSourceChanged {
592 path: self.sources[file_index].path.display().to_string(),
593 }
594 } else {
595 Par2Error::Io(error)
596 }
597 })?;
598 Ok(take)
599 }
600}
601
602#[allow(clippy::too_many_arguments)]
603fn resolve_source_identity(
604 base: &Path,
605 input: &Path,
606 block_size: u64,
607 cancellation: &CancellationToken,
608 progress: Option<&ProgressCallback>,
609 file_index: u32,
610 file_total: u32,
611 bytes_processed: &AtomicU64,
612 total_bytes: u64,
613 cache: Option<&SourceScanCache>,
614) -> Result<CreationSource> {
615 if block_size == 0 || !block_size.is_multiple_of(4) {
616 return Err(Par2Error::InvalidCreationOptions {
617 reason: format!("source block size {block_size} is not a positive multiple of four"),
618 });
619 }
620 let path = resolve_input_path(base, input)?;
621 let metadata = fs::metadata(&path).map_err(Par2Error::Io)?;
622 if !metadata.is_file() {
623 return Err(Par2Error::UnsafeCreationSource {
624 path: input.display().to_string(),
625 reason: "source is not a regular file".to_string(),
626 });
627 }
628 let fingerprint = SourceFingerprint::from_metadata(&metadata);
629 let relative = path
630 .strip_prefix(base)
631 .map_err(|_| Par2Error::UnsafeCreationSource {
632 path: input.display().to_string(),
633 reason: "source is outside the base directory".to_string(),
634 })?;
635 validate_relative_path(relative, input)?;
636 let relative_name = relative
637 .to_str()
638 .ok_or_else(|| Par2Error::UnsafeCreationSource {
639 path: input.display().to_string(),
640 reason: "source filename is not valid UTF-8".to_string(),
641 })?;
642 let par2_name = relative_name.replace('\\', "/");
643 let par2_name = translate_par2_name_to_relative(&par2_name).map_err(|error| {
644 Par2Error::UnsafeCreationSource {
645 path: input.display().to_string(),
646 reason: error.to_string(),
647 }
648 })?;
649 if par2_name.is_empty()
650 || par2_name.len() > MAX_PAR2_NAME_BYTES
651 || par2_name.as_bytes().contains(&0)
652 {
653 return Err(Par2Error::UnsafeCreationSource {
654 path: input.display().to_string(),
655 reason: "PAR2 name is empty, contains NUL, or exceeds 100000 bytes".to_string(),
656 });
657 }
658
659 if let Some(source) = cache.and_then(|cache| cache.take(&path, &fingerprint, block_size)) {
666 debug_assert_eq!(source.par2_name, par2_name);
667 report_scan_progress(
668 ScanProgress {
669 progress,
670 file_index,
671 file_total,
672 bytes_processed,
673 total_bytes,
674 },
675 fingerprint.length,
676 block_size,
677 source.slice_checksums.len(),
678 cancellation,
679 )?;
680 return Ok(source);
681 }
682
683 let mut file = File::open(&path).map_err(Par2Error::Io)?;
684 let slice_count = if fingerprint.length == 0 {
685 0
686 } else {
687 fingerprint
688 .length
689 .checked_add(block_size - 1)
690 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
691 reason: "source slice count overflows".to_string(),
692 })?
693 / block_size
694 };
695 if slice_count > MAX_SLICES_PER_FILE as u64 {
696 return Err(Par2Error::ResourceLimitExceeded {
697 reason: format!("source slice count {slice_count} exceeds {MAX_SLICES_PER_FILE}"),
698 });
699 }
700 let slice_count_usize =
701 usize::try_from(slice_count).map_err(|_| Par2Error::ResourceLimitExceeded {
702 reason: "source slice count exceeds addressable memory".to_string(),
703 })?;
704
705 let head_len = usize::try_from(FIRST_HASH_BYTES.min(fingerprint.length)).map_err(|_| {
710 Par2Error::ResourceLimitExceeded {
711 reason: "source head length exceeds addressable memory".to_string(),
712 }
713 })?;
714 let mut head = vec![0u8; head_len];
715 read_exact_or_changed(&mut file, &mut head, input)?;
716 let mut first_hash = Md5State::new();
717 first_hash.update(&head);
718 drop(head);
719
720 let after = fs::metadata(&path).map_err(Par2Error::Io)?;
721 if SourceFingerprint::from_metadata(&after) != fingerprint {
722 return Err(Par2Error::CreationSourceChanged {
723 path: input.display().to_string(),
724 });
725 }
726
727 let hash_16k = first_hash.finalize();
728 let mut file_id_hash = Md5State::new();
729 file_id_hash.update(&hash_16k);
730 file_id_hash.update(&fingerprint.length.to_le_bytes());
731 file_id_hash.update(par2_name.as_bytes());
732 let file_id = FileId::from_bytes(file_id_hash.finalize());
733 report_scan_progress(
737 ScanProgress {
738 progress,
739 file_index,
740 file_total,
741 bytes_processed,
742 total_bytes,
743 },
744 fingerprint.length,
745 block_size,
746 slice_count_usize,
747 cancellation,
748 )?;
749
750 let source = CreationSource {
751 path,
752 par2_name,
753 file_id,
754 file_length: fingerprint.length,
755 hash_full: [0; 16],
756 hash_16k,
757 slice_checksums: vec![DEFERRED_SLICE_CHECKSUM; slice_count_usize],
758 };
759 if let Some(cache) = cache {
760 cache.store(fingerprint, block_size, &source);
761 }
762 Ok(source)
763}
764
765fn resolve_input_path(base: &Path, input: &Path) -> Result<PathBuf> {
766 if input.is_absolute() {
767 return fs::canonicalize(input).map_err(Par2Error::Io);
768 }
769
770 fs::canonicalize(base.join(input)).map_err(Par2Error::Io)
771}
772
773fn validate_relative_path(relative: &Path, input: &Path) -> Result<()> {
774 if relative.as_os_str().is_empty()
775 || relative.components().any(|component| {
776 matches!(
777 component,
778 Component::ParentDir | Component::RootDir | Component::Prefix(_)
779 )
780 })
781 {
782 return Err(Par2Error::UnsafeCreationSource {
783 path: input.display().to_string(),
784 reason: "source does not have a safe relative path".to_string(),
785 });
786 }
787 Ok(())
788}
789
790struct ScanProgress<'a> {
792 progress: Option<&'a ProgressCallback>,
793 file_index: u32,
794 file_total: u32,
795 bytes_processed: &'a AtomicU64,
796 total_bytes: u64,
797}
798
799fn report_scan_progress(
801 context: ScanProgress<'_>,
802 file_length: u64,
803 block_size: u64,
804 slice_count: usize,
805 cancellation: &CancellationToken,
806) -> Result<()> {
807 let mut remaining = file_length;
808 for _ in 0..slice_count {
809 if cancellation.is_cancelled() {
810 return Err(Par2Error::Cancelled);
811 }
812 let step = remaining.min(block_size);
813 remaining -= step;
814 let processed_total = context
815 .bytes_processed
816 .fetch_add(step, Ordering::Relaxed)
817 .saturating_add(step);
818 report_progress(
819 context.progress,
820 context.file_index,
821 context.file_total,
822 processed_total,
823 context.total_bytes,
824 );
825 }
826 Ok(())
827}
828
829pub(crate) struct SourceContentHashes {
832 pub(crate) hash_full: [u8; 16],
833 pub(crate) hash_16k: [u8; 16],
834 pub(crate) slice_checksums: Vec<SliceChecksum>,
835}
836
837fn hash_source_contents(
844 path: &Path,
845 input: &Path,
846 file_length: u64,
847 block_size: u64,
848 slice_count: usize,
849 cancellation: &CancellationToken,
850) -> Result<SourceContentHashes> {
851 let block_size_usize =
852 usize::try_from(block_size).map_err(|_| Par2Error::ResourceLimitExceeded {
853 reason: format!("source block size {block_size} exceeds addressable memory"),
854 })?;
855 let mut file = File::open(path).map_err(Par2Error::Io)?;
856 let mut full_hash = FileHashState::new();
857 let mut first_hash = Md5State::new();
858 let mut first_bytes = 0u64;
859 let mut checksums = Vec::with_capacity(slice_count);
860
861 let slice_len = |slice_index: usize| -> Result<usize> {
864 let offset = (slice_index as u64)
865 .checked_mul(block_size)
866 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
867 reason: "source slice offset overflows".to_string(),
868 })?;
869 usize::try_from(file_length.saturating_sub(offset).min(block_size)).map_err(|_| {
870 Par2Error::ResourceLimitExceeded {
871 reason: "source slice length exceeds addressable memory".to_string(),
872 }
873 })
874 };
875
876 let lanes = create_md5_batch_lanes(block_size_usize);
877 if lanes >= 2 {
878 let mut batch = vec![0u8; lanes * block_size_usize];
884 let mut digests = vec![[0u8; 16]; lanes];
885 let mut lens = vec![0usize; lanes];
886 let mut crcs = vec![0u32; lanes];
887 let mut slice_index = 0usize;
888
889 while slice_index < slice_count {
890 let batch_slices = lanes.min(slice_count - slice_index);
891
892 for lane in 0..batch_slices {
893 if cancellation.is_cancelled() {
894 return Err(Par2Error::Cancelled);
895 }
896 let actual_len = slice_len(slice_index + lane)?;
897 let start = lane * block_size_usize;
898 let slice = &mut batch[start..start + actual_len];
899 read_exact_or_changed(&mut file, slice, input)?;
900 lens[lane] = actual_len;
901
902 let slice = &batch[start..start + actual_len];
909 absorb_file_stream(&mut full_hash, &mut first_hash, &mut first_bytes, slice);
910 crcs[lane] = checksum::crc32_padded(slice, block_size);
911 }
912
913 let inputs = (0..batch_slices)
914 .map(|lane| {
915 let start = lane * block_size_usize;
916 &batch[start..start + lens[lane]]
917 })
918 .collect::<Vec<_>>();
919 md5_simd::md5_multi_into(&inputs, Some(block_size), &mut digests[..batch_slices]);
920
921 for lane in 0..batch_slices {
922 checksums.push(SliceChecksum {
923 crc32: crcs[lane],
924 md5: digests[lane],
925 });
926 }
927
928 slice_index += batch_slices;
929 }
930 } else {
931 let mut buffer = vec![0u8; READ_BUFFER_BYTES.min(block_size_usize.max(1))];
935 for slice_index in 0..slice_count {
936 if cancellation.is_cancelled() {
937 return Err(Par2Error::Cancelled);
938 }
939 let actual_len = slice_len(slice_index)?;
940 let mut remaining = actual_len;
941 let mut slice_hash = SliceChecksumState::new();
942 while remaining > 0 {
943 if cancellation.is_cancelled() {
944 return Err(Par2Error::Cancelled);
945 }
946 let take = remaining.min(buffer.len());
947 read_exact_or_changed(&mut file, &mut buffer[..take], input)?;
948 let chunk = &buffer[..take];
949 slice_hash.update(chunk);
950 absorb_file_stream(&mut full_hash, &mut first_hash, &mut first_bytes, chunk);
951 remaining -= take;
952 }
953 let pad_to = ((actual_len as u64) < block_size).then_some(block_size);
954 let (crc32, md5) = slice_hash.finalize(pad_to);
955 checksums.push(SliceChecksum { crc32, md5 });
956 }
957 }
958
959 if full_hash.bytes_fed() != file_length {
960 return Err(Par2Error::CreationSourceChanged {
961 path: input.display().to_string(),
962 });
963 }
964 Ok(SourceContentHashes {
965 hash_full: full_hash.finalize(),
966 hash_16k: first_hash.finalize(),
967 slice_checksums: checksums,
968 })
969}
970
971pub(crate) fn hydrate_source_hashes(
979 sources: &mut [CreationSource],
980 block_size: u64,
981 cancellation: &CancellationToken,
982) -> Result<()> {
983 let hash_one = |source: &mut CreationSource| -> Result<()> {
984 if cancellation.is_cancelled() {
985 return Err(Par2Error::Cancelled);
986 }
987 let metadata = fs::metadata(&source.path).map_err(Par2Error::Io)?;
988 if !metadata.is_file() || metadata.len() != source.file_length {
989 return Err(Par2Error::CreationSourceChanged {
990 path: source.path.display().to_string(),
991 });
992 }
993 let hashes = hash_source_contents(
994 &source.path,
995 &source.path,
996 source.file_length,
997 block_size,
998 source.slice_checksums.len(),
999 cancellation,
1000 )?;
1001 if hashes.hash_16k != source.hash_16k {
1002 return Err(Par2Error::CreationSourceChanged {
1003 path: source.path.display().to_string(),
1004 });
1005 }
1006 source.hash_full = hashes.hash_full;
1007 source.slice_checksums = hashes.slice_checksums;
1008 Ok(())
1009 };
1010 let scan_parallel = reedsolomon_rs::threading::parallel_enabled()
1013 && sources.len() > 1
1014 && super::encode::configured_create_threads() != 1;
1015 if scan_parallel {
1016 sources
1017 .par_iter_mut()
1018 .map(hash_one)
1019 .collect::<Vec<Result<()>>>()
1020 .into_iter()
1021 .collect::<Result<Vec<()>>>()?;
1022 } else {
1023 for source in sources.iter_mut() {
1024 hash_one(source)?;
1025 }
1026 }
1027 Ok(())
1028}
1029
1030pub(crate) struct FusedSourceHashes {
1032 hash_full: Vec<[u8; 16]>,
1033 slice_checksums: Vec<Vec<SliceChecksum>>,
1034}
1035
1036impl FusedSourceHashes {
1037 pub(crate) fn apply(self, sources: &mut [CreationSource]) -> Result<()> {
1039 if self.hash_full.len() != sources.len() || self.slice_checksums.len() != sources.len() {
1040 return Err(Par2Error::InvalidCreationOptions {
1041 reason: "fused source hashes do not cover the recovery set".to_string(),
1042 });
1043 }
1044 for ((source, hash_full), checksums) in sources
1045 .iter_mut()
1046 .zip(self.hash_full)
1047 .zip(self.slice_checksums)
1048 {
1049 if checksums.len() != source.slice_checksums.len() {
1050 return Err(Par2Error::CreationSourceChanged {
1051 path: source.path.display().to_string(),
1052 });
1053 }
1054 source.hash_full = hash_full;
1055 source.slice_checksums = checksums;
1056 }
1057 Ok(())
1058 }
1059}
1060
1061pub(crate) struct FusedSourceHasher<'a> {
1073 sources: &'a [CreationSource],
1074 slice_ends: Vec<usize>,
1076 block_size: u64,
1077 next_source_index: usize,
1078 file_index: usize,
1079 full_hash: FileHashState,
1080 first_hash: Md5State,
1081 first_bytes: u64,
1082 hash_full: Vec<[u8; 16]>,
1083 slice_checksums: Vec<Vec<SliceChecksum>>,
1084 digests: Vec<[u8; 16]>,
1085}
1086
1087impl<'a> FusedSourceHasher<'a> {
1088 pub(crate) fn new(sources: &'a [CreationSource], block_size: u64) -> Result<Self> {
1089 if block_size == 0 {
1090 return Err(Par2Error::InvalidCreationOptions {
1091 reason: "fused source hashing needs a positive block size".to_string(),
1092 });
1093 }
1094 let mut slice_ends = Vec::with_capacity(sources.len());
1095 let mut end = 0usize;
1096 for source in sources {
1097 end = end
1098 .checked_add(source.slice_checksums.len())
1099 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
1100 reason: "source slice index overflows".to_string(),
1101 })?;
1102 slice_ends.push(end);
1103 }
1104 Ok(Self {
1105 sources,
1106 slice_ends,
1107 block_size,
1108 next_source_index: 0,
1109 file_index: 0,
1110 full_hash: FileHashState::new(),
1111 first_hash: Md5State::new(),
1112 first_bytes: 0,
1113 hash_full: Vec::with_capacity(sources.len()),
1114 slice_checksums: sources
1115 .iter()
1116 .map(|source| Vec::with_capacity(source.slice_checksums.len()))
1117 .collect(),
1118 digests: Vec::new(),
1119 })
1120 }
1121
1122 fn finish_file(&mut self) -> Result<()> {
1123 let source =
1124 self.sources
1125 .get(self.file_index)
1126 .ok_or_else(|| Par2Error::InvalidCreationOptions {
1127 reason: "fused source hashing ran past the recovery set".to_string(),
1128 })?;
1129 if self.full_hash.bytes_fed() != source.file_length {
1130 return Err(Par2Error::CreationSourceChanged {
1131 path: source.path.display().to_string(),
1132 });
1133 }
1134 let hash_16k = std::mem::replace(&mut self.first_hash, Md5State::new()).finalize();
1135 if hash_16k != source.hash_16k {
1136 return Err(Par2Error::CreationSourceChanged {
1137 path: source.path.display().to_string(),
1138 });
1139 }
1140 self.hash_full
1141 .push(std::mem::take(&mut self.full_hash).finalize());
1142 self.first_bytes = 0;
1143 self.file_index += 1;
1144 Ok(())
1145 }
1146
1147 pub(crate) fn finish(mut self) -> Result<FusedSourceHashes> {
1149 while self.file_index < self.sources.len() {
1150 self.finish_file()?;
1151 }
1152 if self.next_source_index != self.slice_ends.last().copied().unwrap_or(0) {
1153 return Err(Par2Error::InvalidCreationOptions {
1154 reason: "fused source hashing did not see every source slice".to_string(),
1155 });
1156 }
1157 Ok(FusedSourceHashes {
1158 hash_full: self.hash_full,
1159 slice_checksums: self.slice_checksums,
1160 })
1161 }
1162}
1163
1164impl ForwardSourceObserver for FusedSourceHasher<'_> {
1165 fn observe_slices(&mut self, first_source_index: usize, slices: &[&[u8]]) -> Result<()> {
1166 if first_source_index != self.next_source_index {
1167 return Err(Par2Error::InvalidCreationOptions {
1168 reason: "fused source hashing needs the feed in source order".to_string(),
1169 });
1170 }
1171 if slices.len() > self.digests.len() {
1172 self.digests.resize(slices.len(), [0u8; 16]);
1173 }
1174 md5_simd::md5_multi_into(
1178 slices,
1179 Some(self.block_size),
1180 &mut self.digests[..slices.len()],
1181 );
1182 for (offset, bytes) in slices.iter().enumerate() {
1183 let index = first_source_index + offset;
1184 while self
1185 .slice_ends
1186 .get(self.file_index)
1187 .is_some_and(|&end| index >= end)
1188 {
1189 self.finish_file()?;
1190 }
1191 if self.file_index >= self.sources.len() {
1192 return Err(Par2Error::InvalidCreationOptions {
1193 reason: "fused source hashing ran past the recovery set".to_string(),
1194 });
1195 }
1196 absorb_file_stream(
1197 &mut self.full_hash,
1198 &mut self.first_hash,
1199 &mut self.first_bytes,
1200 bytes,
1201 );
1202 let crc32 = checksum::crc32_padded(bytes, self.block_size);
1203 self.slice_checksums[self.file_index].push(SliceChecksum {
1204 crc32,
1205 md5: self.digests[offset],
1206 });
1207 }
1208 self.next_source_index = first_source_index + slices.len();
1209 Ok(())
1210 }
1211}
1212
1213fn absorb_file_stream(
1221 full_hash: &mut FileHashState,
1222 first_hash: &mut Md5State,
1223 first_bytes: &mut u64,
1224 chunk: &[u8],
1225) {
1226 full_hash.update(chunk);
1227 if *first_bytes < FIRST_HASH_BYTES {
1228 let take = (FIRST_HASH_BYTES - *first_bytes).min(chunk.len() as u64) as usize;
1229 first_hash.update(&chunk[..take]);
1230 *first_bytes += take as u64;
1231 }
1232}
1233
1234fn read_exact_or_changed(file: &mut File, buffer: &mut [u8], input: &Path) -> Result<()> {
1235 file.read_exact(buffer).map_err(|error| {
1236 if error.kind() == io::ErrorKind::UnexpectedEof {
1237 Par2Error::CreationSourceChanged {
1238 path: input.display().to_string(),
1239 }
1240 } else {
1241 Par2Error::Io(error)
1242 }
1243 })
1244}
1245
1246fn report_progress(
1247 progress: Option<&ProgressCallback>,
1248 current: u32,
1249 total: u32,
1250 bytes_processed: u64,
1251 total_bytes: u64,
1252) {
1253 if let Some(progress) = progress {
1254 progress(crate::types::ProgressUpdate {
1255 stage: ProgressStage::Creating,
1256 current,
1257 total,
1258 bytes_processed,
1259 total_bytes: Some(total_bytes),
1260 phase: crate::types::ProgressPhase::SourceScan,
1261 });
1262 }
1263}
1264
1265#[cfg(test)]
1266mod tests {
1267 use super::*;
1268
1269 fn planned_source(
1270 par2_name: &str,
1271 file_length: u64,
1272 block_size: u64,
1273 head: &[u8],
1274 ) -> CreationSource {
1275 let mut first = Md5State::new();
1276 first.update(head);
1277 let hash_16k = first.finalize();
1278 let mut file_id_hash = Md5State::new();
1279 file_id_hash.update(&hash_16k);
1280 file_id_hash.update(&file_length.to_le_bytes());
1281 file_id_hash.update(par2_name.as_bytes());
1282 let slice_count = usize::try_from(file_length.div_ceil(block_size)).unwrap();
1283 CreationSource {
1284 path: PathBuf::from(par2_name),
1285 par2_name: par2_name.to_string(),
1286 file_id: FileId::from_bytes(file_id_hash.finalize()),
1287 file_length,
1288 hash_full: [0; 16],
1289 hash_16k,
1290 slice_checksums: vec![DEFERRED_SLICE_CHECKSUM; slice_count],
1291 }
1292 }
1293
1294 #[test]
1298 fn the_fused_hasher_refuses_a_feed_that_is_not_in_source_order() {
1299 let payload = [7u8; 16];
1300 let sources = [planned_source("a.bin", 16, 8, &payload)];
1301 let mut hasher = FusedSourceHasher::new(&sources, 8).unwrap();
1302 assert!(matches!(
1303 hasher.observe_slices(1, &[&payload[8..]]),
1304 Err(Par2Error::InvalidCreationOptions { .. })
1305 ));
1306 }
1307
1308 #[test]
1312 fn the_fused_hasher_rejects_bytes_that_differ_from_the_planned_identity() {
1313 let payload = [7u8; 16];
1314 let sources = [planned_source("a.bin", 16, 8, &payload)];
1315 let mut hasher = FusedSourceHasher::new(&sources, 8).unwrap();
1316 let changed = [9u8; 16];
1317 hasher
1318 .observe_slices(0, &[&changed[..8], &changed[8..]])
1319 .unwrap();
1320 assert!(matches!(
1321 hasher.finish(),
1322 Err(Par2Error::CreationSourceChanged { .. })
1323 ));
1324 }
1325
1326 #[test]
1328 fn the_fused_hasher_matches_a_direct_read_of_the_same_bytes() {
1329 let payload: Vec<u8> = (0..20u8).collect();
1330 let sources = [planned_source("a.bin", payload.len() as u64, 8, &payload)];
1331 let mut hasher = FusedSourceHasher::new(&sources, 8).unwrap();
1332 hasher
1333 .observe_slices(0, &[&payload[0..8], &payload[8..16], &payload[16..20]])
1334 .unwrap();
1335 let mut hydrated = sources.to_vec();
1336 hasher.finish().unwrap().apply(&mut hydrated).unwrap();
1337 assert_eq!(hydrated[0].hash_full, checksum::md5(&payload));
1338 assert_eq!(hydrated[0].slice_checksums.len(), 3);
1339 assert_eq!(
1340 hydrated[0].slice_checksums[2].md5,
1341 md5_simd::md5_multi(&[&payload[16..20]], Some(8))[0]
1342 );
1343 assert_eq!(
1344 hydrated[0].slice_checksums[2].crc32,
1345 checksum::crc32_padded(&payload[16..20], 8)
1346 );
1347 }
1348
1349 #[test]
1350 fn main_file_count_boundary_matches_parser_limit() {
1351 assert!(validate_main_file_count(MAX_FILES_PER_SET).is_ok());
1352 assert!(matches!(
1353 validate_main_file_count(MAX_FILES_PER_SET + 1),
1354 Err(Par2Error::ResourceLimitExceeded { .. })
1355 ));
1356 }
1357}