1use std::fs;
2use std::mem::size_of;
3use std::path::{Path, PathBuf};
4
5use crate::checksum::md5;
6use crate::error::{Par2Error, Result};
7use crate::types::{FileId, RecoveryExponent, RecoverySetId, SliceChecksum};
8
9use super::encode::{ForwardKernel, estimate_forward_memory};
10use super::metal::estimate_processing_memory;
11use super::options::{BlockSizing, CreationBackend, Par2CreatorOptions, RecoveryAmount};
12use super::output::{
13 TargetSnapshot, capture_target_snapshot, estimate_critical_packet_bytes,
14 estimate_packet_build_workspace_bytes, estimate_transaction_workspace_bytes,
15 estimate_validation_workspace_bytes,
16};
17use super::source::{CreationSource, InputLength, collect_input_lengths, collect_sources};
18use super::volume::{RecoveryVolumePlan, allocate_volumes};
19
20const AUTO_SOURCE_SLICE_TARGET: u64 = 2_000;
21const MAX_RECOVERY_EXPONENT: u32 = 65_535;
22const MEMORY_FLOOR_BYTES: usize = 256 * 1024 * 1024;
23const MEMORY_32_BIT_CAP_BYTES: usize = 1024 * 1024 * 1024;
24pub(crate) fn default_memory_limit() -> usize {
25 memory_limit_for(physical_memory_bytes(), usize::BITS)
26}
27
28fn memory_limit_for(physical_memory: Option<u64>, address_bits: u32) -> usize {
29 let mut limit = physical_memory
30 .and_then(|bytes| usize::try_from(bytes / 8).ok())
31 .unwrap_or(MEMORY_FLOOR_BYTES)
32 .max(MEMORY_FLOOR_BYTES);
33 if address_bits < 64 {
34 limit = limit.min(MEMORY_32_BIT_CAP_BYTES);
35 }
36 limit
37}
38
39pub(crate) fn controller_overhead_blocks(source_blocks: u32) -> usize {
40 2 + 24usize.min(source_blocks as usize + 1)
41}
42
43#[cfg(unix)]
44fn physical_memory_bytes() -> Option<u64> {
45 let pages = unsafe { libc::sysconf(libc::_SC_PHYS_PAGES) };
46 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
47 if pages <= 0 || page_size <= 0 {
48 return None;
49 }
50 (pages as u64).checked_mul(page_size as u64)
51}
52
53#[cfg(target_os = "windows")]
54#[repr(C)]
55struct WindowsMemoryStatusEx {
56 length: u32,
57 memory_load: u32,
58 total_phys: u64,
59 available_phys: u64,
60 total_page_file: u64,
61 available_page_file: u64,
62 total_virtual: u64,
63 available_virtual: u64,
64 available_extended_virtual: u64,
65}
66
67#[cfg(target_os = "windows")]
68unsafe extern "system" {
69 fn GlobalMemoryStatusEx(status: *mut WindowsMemoryStatusEx) -> i32;
70}
71
72#[cfg(target_os = "windows")]
73fn physical_memory_bytes() -> Option<u64> {
74 let mut status = WindowsMemoryStatusEx {
75 length: std::mem::size_of::<WindowsMemoryStatusEx>() as u32,
76 memory_load: 0,
77 total_phys: 0,
78 available_phys: 0,
79 total_page_file: 0,
80 available_page_file: 0,
81 total_virtual: 0,
82 available_virtual: 0,
83 available_extended_virtual: 0,
84 };
85 let success = unsafe { GlobalMemoryStatusEx(&mut status) } != 0;
88 success.then_some(status.total_phys)
89}
90
91#[cfg(not(any(unix, target_os = "windows")))]
92fn physical_memory_bytes() -> Option<u64> {
93 None
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct Par2MemoryPlan {
99 pub source_metadata_bytes: usize,
102 pub source_hash_workspace_bytes: usize,
104 pub critical_packet_bytes: usize,
106 pub main_file_id_workspace_bytes: usize,
108 pub packet_build_workspace_bytes: usize,
111 pub transaction_workspace_bytes: usize,
113 pub validation_workspace_bytes: usize,
116 pub processing_buffer_limit_bytes: usize,
118 pub processing_peak_bytes: usize,
120 pub total_creation_peak_bytes: usize,
122 pub factor_workspace_bytes: usize,
127 pub jit_workspace_bytes: usize,
129 pub stripe_buffer_bytes: usize,
131 pub controller_overhead_blocks: usize,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct Par2CreatePlan {
138 pub base_path: PathBuf,
140 pub output_stem: PathBuf,
142 pub main_path: PathBuf,
144 pub volumes: Vec<RecoveryVolumePlan>,
146 pub volume_scheme: super::options::VolumeScheme,
148 pub volume_paths: Vec<PathBuf>,
150 pub output_paths: Vec<PathBuf>,
152 pub(crate) target_snapshots: Vec<TargetSnapshot>,
154 pub sources: Vec<CreationSource>,
156 pub slice_size: u64,
158 pub source_slice_count: u32,
160 pub recovery_count: u32,
162 pub first_exponent: RecoveryExponent,
164 pub recovery_exponents: Vec<RecoveryExponent>,
166 pub recovery_set_id: RecoverySetId,
168 pub forward_kernel: ForwardKernel,
170 pub backend: CreationBackend,
172 pub memory: Par2MemoryPlan,
174 pub dry_run: bool,
176 pub skipped_empty: Vec<PathBuf>,
187}
188
189impl Par2CreatePlan {
190 pub fn file_count(&self) -> usize {
192 self.sources.len()
193 }
194
195 pub fn volume_count(&self) -> usize {
197 self.volumes.len()
198 }
199
200 pub(crate) fn validate_integrity(&self) -> Result<()> {
201 if self.sources.is_empty()
202 || self.output_paths.len() != self.volume_paths.len() + 1
203 || self.output_paths.first() != Some(&self.main_path)
204 || self.volumes.len() != self.volume_paths.len()
205 || self.target_snapshots.len() != self.output_paths.len()
206 || self.recovery_exponents.len() != self.recovery_count as usize
207 {
208 return Err(Par2Error::InvalidCreationOptions {
209 reason: "creation plan structure is inconsistent".to_string(),
210 });
211 }
212 if self.slice_size == 0 || !self.slice_size.is_multiple_of(4) {
213 return Err(Par2Error::InvalidCreationOptions {
214 reason: "creation plan has an invalid slice size".to_string(),
215 });
216 }
217 if self.first_exponent > 32_768 {
218 return Err(Par2Error::InvalidCreationOptions {
219 reason: "creation plan first exponent is out of range".to_string(),
220 });
221 }
222 let expected_end = self
223 .first_exponent
224 .checked_add(self.recovery_count)
225 .filter(|end| *end < 65_536)
226 .ok_or_else(|| Par2Error::InvalidCreationOptions {
227 reason: "creation plan recovery exponent range is out of range".to_string(),
228 })?;
229 if self
230 .sources
231 .windows(2)
232 .any(|pair| pair[0].file_id >= pair[1].file_id)
233 {
234 return Err(Par2Error::InvalidCreationOptions {
235 reason: "creation plan sources are not strictly sorted".to_string(),
236 });
237 }
238 let source_slice_count = self.sources.iter().try_fold(0u32, |total, source| {
239 total.checked_add(source.slice_count()).ok_or_else(|| {
240 Par2Error::InvalidCreationOptions {
241 reason: "creation plan source slice count overflows".to_string(),
242 }
243 })
244 })?;
245 if source_slice_count != self.source_slice_count {
246 return Err(Par2Error::InvalidCreationOptions {
247 reason: "creation plan source slice count differs from sources".to_string(),
248 });
249 }
250 let cpu_memory = estimate_forward_memory(
251 self.slice_size,
252 self.source_slice_count as usize,
253 self.recovery_count as usize,
254 self.memory.processing_buffer_limit_bytes,
255 self.forward_kernel,
256 )?;
257 let forward_memory = estimate_processing_memory(
258 self.backend,
259 usize::try_from(self.slice_size).map_err(|_| Par2Error::ResourceLimitExceeded {
260 reason: "slice size exceeds addressable memory".to_string(),
261 })?,
262 self.source_slice_count as usize,
263 self.recovery_count as usize,
264 self.memory.processing_buffer_limit_bytes,
265 cpu_memory,
266 )?;
267 let expected_memory = memory_plan_for(
268 &self.sources,
269 self.sources.len(),
270 self.source_slice_count,
271 self.slice_size,
272 MemoryPlanPaths {
273 base_path: &self.base_path,
274 output_stem: &self.output_stem,
275 main_path: &self.main_path,
276 output_paths: &self.output_paths,
277 },
278 &self.volumes,
279 self.memory.processing_buffer_limit_bytes,
280 forward_memory,
281 )?;
282 if self.memory != expected_memory {
283 return Err(Par2Error::InvalidCreationOptions {
284 reason: "creation plan memory estimate differs from sources".to_string(),
285 });
286 }
287 let output_parent =
288 self.main_path
289 .parent()
290 .ok_or_else(|| Par2Error::InvalidCreationOptions {
291 reason: "creation plan main path has no parent".to_string(),
292 })?;
293 if self.output_stem.parent() != Some(output_parent)
294 || self.output_stem.file_name().is_none()
295 || self.main_path.file_stem() != self.output_stem.file_name()
296 || self
297 .main_path
298 .extension()
299 .and_then(|extension| extension.to_str())
300 .is_none_or(|extension| !extension.eq_ignore_ascii_case("par2"))
301 || self
302 .volume_paths
303 .iter()
304 .zip(&self.volumes)
305 .any(|(path, volume)| path != &output_parent.join(&volume.filename))
306 {
307 return Err(Par2Error::InvalidCreationOptions {
308 reason: "creation plan output paths do not match the naming contract".to_string(),
309 });
310 }
311 let mut main_body = Vec::with_capacity(12 + self.sources.len() * 16);
312 main_body.extend_from_slice(&self.slice_size.to_le_bytes());
313 main_body.extend_from_slice(&(self.sources.len() as u32).to_le_bytes());
314 for source in &self.sources {
315 main_body.extend_from_slice(source.file_id.as_bytes());
316 }
317 if RecoverySetId::from_bytes(md5(&main_body)) != self.recovery_set_id {
318 return Err(Par2Error::InvalidCreationOptions {
319 reason: "creation plan recovery-set identifier differs from Main packet"
320 .to_string(),
321 });
322 }
323 let mut exponent = self.first_exponent;
324 for (index, volume) in self.volumes.iter().enumerate() {
325 if self.output_paths[index + 1] != self.volume_paths[index]
326 || volume.first_exponent != exponent
327 || volume.recovery_count == 0
328 {
329 return Err(Par2Error::InvalidCreationOptions {
330 reason: "creation plan volume allocation is inconsistent".to_string(),
331 });
332 }
333 exponent = exponent.checked_add(volume.recovery_count).ok_or_else(|| {
334 Par2Error::InvalidCreationOptions {
335 reason: "creation plan recovery exponent range overflows".to_string(),
336 }
337 })?;
338 }
339 if exponent != expected_end {
340 return Err(Par2Error::InvalidCreationOptions {
341 reason: "creation plan recovery allocation does not sum to recovery count"
342 .to_string(),
343 });
344 }
345 for (offset, exponent) in self.recovery_exponents.iter().enumerate() {
346 let expected = self.first_exponent.checked_add(offset as u32);
347 if expected != Some(*exponent) {
348 return Err(Par2Error::InvalidCreationOptions {
349 reason: "creation plan recovery exponents are not contiguous".to_string(),
350 });
351 }
352 }
353 Ok(())
354 }
355}
356
357struct MemoryPlanPaths<'a> {
358 base_path: &'a Path,
359 output_stem: &'a Path,
360 main_path: &'a Path,
361 output_paths: &'a [PathBuf],
362}
363
364#[allow(clippy::too_many_arguments)]
365fn memory_plan_for(
366 sources: &[CreationSource],
367 input_count: usize,
368 source_slice_count: u32,
369 block_size: u64,
370 paths: MemoryPlanPaths<'_>,
371 volumes: &[RecoveryVolumePlan],
372 processing_buffer_limit_bytes: usize,
373 forward_memory: super::encode::ForwardMemoryEstimate,
374) -> Result<Par2MemoryPlan> {
375 if processing_buffer_limit_bytes == 0 {
376 return Err(Par2Error::ResourceLimitExceeded {
377 reason: "memory limit must be greater than zero".to_string(),
378 });
379 }
380 let source_metadata_bytes = estimate_source_metadata_bytes(sources, input_count)?;
381 let source_hash_workspace_bytes = estimate_source_hash_workspace(input_count, block_size)?;
382 let critical_packet_bytes = estimate_critical_packet_bytes(sources)?;
383 let main_file_id_workspace_bytes = estimate_main_file_id_workspace_bytes(sources)?;
384 let packet_build_workspace_bytes = estimate_packet_build_workspace_bytes(sources)?;
385 let transaction_workspace_bytes = estimate_transaction_workspace_bytes(
386 paths.base_path,
387 paths.output_stem,
388 paths.main_path,
389 paths.output_paths,
390 volumes,
391 sources,
392 volumes
393 .iter()
394 .try_fold(0u32, |total, volume| {
395 total.checked_add(volume.recovery_count)
396 })
397 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
398 reason: "recovery volume count estimate overflows".to_string(),
399 })?,
400 )?;
401 let validation_workspace_bytes = estimate_validation_workspace_bytes(
402 sources,
403 paths.output_paths,
404 volumes,
405 critical_packet_bytes,
406 )?;
407 let plan_phase = checked_memory_add(
408 source_metadata_bytes,
409 source_hash_workspace_bytes,
410 "source creation estimate overflows",
411 )?;
412 let create_phase = [
413 source_hash_workspace_bytes,
414 checked_memory_mul(
415 source_metadata_bytes,
416 2,
417 "source creation estimate overflows",
418 )?,
419 critical_packet_bytes,
420 main_file_id_workspace_bytes,
421 packet_build_workspace_bytes,
422 transaction_workspace_bytes,
423 forward_memory.processing_peak_bytes,
424 ]
425 .into_iter()
426 .try_fold(0usize, |total, bytes| {
427 checked_memory_add(total, bytes, "creation peak estimate overflows")
428 })?;
429 let validation_phase = [
430 checked_memory_mul(
431 source_metadata_bytes,
432 2,
433 "validation source metadata estimate overflows",
434 )?,
435 critical_packet_bytes,
436 main_file_id_workspace_bytes,
437 packet_build_workspace_bytes,
438 transaction_workspace_bytes,
439 validation_workspace_bytes,
440 ]
441 .into_iter()
442 .try_fold(0usize, |total, bytes| {
443 checked_memory_add(total, bytes, "validation peak estimate overflows")
444 })?;
445
446 Ok(Par2MemoryPlan {
447 source_metadata_bytes,
448 source_hash_workspace_bytes,
449 critical_packet_bytes,
450 main_file_id_workspace_bytes,
451 packet_build_workspace_bytes,
452 transaction_workspace_bytes,
453 validation_workspace_bytes,
454 processing_buffer_limit_bytes,
455 processing_peak_bytes: forward_memory.processing_peak_bytes,
456 total_creation_peak_bytes: plan_phase.max(create_phase).max(validation_phase),
457 factor_workspace_bytes: forward_memory.factor_workspace_bytes,
458 jit_workspace_bytes: forward_memory.jit_workspace_bytes,
459 stripe_buffer_bytes: forward_memory.stripe_buffer_bytes,
460 controller_overhead_blocks: controller_overhead_blocks(source_slice_count),
461 })
462}
463
464fn estimate_source_metadata_bytes(sources: &[CreationSource], capacity: usize) -> Result<usize> {
465 let mut total = checked_memory_mul(
466 capacity,
467 size_of::<CreationSource>(),
468 "source metadata estimate overflows",
469 )?;
470 for source in sources {
471 total = checked_memory_add(
472 total,
473 source
474 .path
475 .as_os_str()
476 .len()
477 .checked_add(64)
478 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
479 reason: "source path estimate overflows".to_string(),
480 })?,
481 "source path estimate overflows",
482 )?;
483 total = checked_memory_add(
484 total,
485 source.par2_name.len().checked_add(64).ok_or_else(|| {
486 Par2Error::ResourceLimitExceeded {
487 reason: "source name estimate overflows".to_string(),
488 }
489 })?,
490 "source name estimate overflows",
491 )?;
492 total = checked_memory_add(
493 total,
494 checked_memory_mul(
495 source.slice_checksums.len(),
496 size_of::<SliceChecksum>(),
497 "source checksum estimate overflows",
498 )?,
499 "source metadata estimate overflows",
500 )?;
501 }
502 Ok(total)
503}
504
505fn estimate_source_hash_workspace(input_count: usize, block_size: u64) -> Result<usize> {
506 const READ_BUFFER_BYTES: usize = 256 * 1024;
507 const HASH_SET_ENTRY_RESERVE_BYTES: usize = 128;
508 let input_lengths = checked_memory_mul(
509 input_count,
510 size_of::<InputLength>(),
511 "source length estimate overflows",
512 )?;
513 let hash_sets = checked_memory_mul(
514 checked_memory_mul(
515 input_count,
516 HASH_SET_ENTRY_RESERVE_BYTES,
517 "source index estimate overflows",
518 )?,
519 2,
520 "source index estimate overflows",
521 )?;
522 let threads = super::encode::configured_create_threads();
526 let concurrent_reads = if threads == 1 || input_count <= 1 {
527 1
528 } else {
529 input_count.min(threads.saturating_add(1))
530 };
531 let block_size_usize = usize::try_from(block_size).unwrap_or(usize::MAX);
541 let lanes = super::source::create_md5_batch_lanes(block_size_usize);
542 let per_task_bytes = if lanes >= 2 {
543 checked_memory_mul(
544 lanes,
545 block_size_usize,
546 "source hash batch estimate overflows",
547 )?
548 .max(READ_BUFFER_BYTES)
549 } else {
550 READ_BUFFER_BYTES
551 };
552 let read_buffers = checked_memory_mul(
553 per_task_bytes,
554 concurrent_reads,
555 "concurrent source read buffer estimate overflows",
556 )?;
557 [read_buffers, input_lengths, hash_sets]
558 .into_iter()
559 .try_fold(0usize, |total, bytes| {
560 checked_memory_add(total, bytes, "source hashing estimate overflows")
561 })
562}
563
564fn estimate_main_file_id_workspace_bytes(sources: &[CreationSource]) -> Result<usize> {
565 checked_memory_add(
566 size_of::<Vec<u8>>(),
567 checked_memory_add(
568 12,
569 checked_memory_mul(
570 sources.len(),
571 size_of::<FileId>(),
572 "Main packet file-ID estimate overflows",
573 )?,
574 "Main packet file-ID estimate overflows",
575 )?,
576 "Main packet file-ID estimate overflows",
577 )
578}
579
580fn checked_memory_add(left: usize, right: usize, reason: &'static str) -> Result<usize> {
581 left.checked_add(right)
582 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
583 reason: reason.to_string(),
584 })
585}
586
587fn checked_memory_mul(left: usize, right: usize, reason: &'static str) -> Result<usize> {
588 left.checked_mul(right)
589 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
590 reason: reason.to_string(),
591 })
592}
593
594pub(crate) fn build_plan_with_cache(
601 options: &Par2CreatorOptions,
602 cache: Option<&super::source::SourceScanCache>,
603) -> Result<Par2CreatePlan> {
604 if options.cancellation.is_cancelled() {
605 return Err(Par2Error::Cancelled);
606 }
607 let output = options
608 .output
609 .as_ref()
610 .ok_or_else(|| Par2Error::InvalidCreationOptions {
611 reason: "an output path or stem is required".to_string(),
612 })?;
613 let (output_parent, output_stem, main_path, stem_name) = normalize_output(output)?;
614 let base_path = match &options.base_path {
615 Some(path) => canonical_source_directory(path)?,
616 None => output_parent.clone(),
617 };
618 let input_lengths = collect_input_lengths(&base_path, &options.inputs, &options.cancellation)?;
619 let total_bytes = input_lengths.iter().try_fold(0u64, |total, input| {
620 total
621 .checked_add(input.length)
622 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
623 reason: "source byte count overflows".to_string(),
624 })
625 })?;
626 let block_size = choose_block_size(&input_lengths, options.block_sizing)?;
627 let collected = collect_sources(
628 &base_path,
629 &options.inputs,
630 block_size,
631 &options.cancellation,
632 options.progress.as_ref(),
633 total_bytes,
634 cache,
635 )?;
636 let skipped_empty = collected.skipped_empty;
637 let mut sources = collected.sources;
638 sources.sort_by_key(|source| source.file_id);
639 if options.cancellation.is_cancelled() {
640 return Err(Par2Error::Cancelled);
641 }
642
643 let source_slice_count = sources.iter().try_fold(0u32, |total, source| {
644 total
645 .checked_add(source.slice_count())
646 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
647 reason: "source slice count overflows u32".to_string(),
648 })
649 })?;
650 let recovery_count = choose_recovery_count(
651 source_slice_count,
652 options.recovery_amount,
653 options.first_exponent,
654 )?;
655 validate_exponent_range(options.first_exponent, recovery_count)?;
656
657 let forward_memory_limit = options.memory_limit.unwrap_or_else(default_memory_limit);
658 if forward_memory_limit == 0 {
659 return Err(Par2Error::ResourceLimitExceeded {
660 reason: "memory limit must be greater than zero".to_string(),
661 });
662 }
663 let cpu_memory = estimate_forward_memory(
664 block_size,
665 source_slice_count as usize,
666 recovery_count as usize,
667 forward_memory_limit,
668 options.forward_kernel,
669 )?;
670 let forward_memory = estimate_processing_memory(
671 options.backend,
672 usize::try_from(block_size).map_err(|_| Par2Error::ResourceLimitExceeded {
673 reason: "slice size exceeds addressable memory".to_string(),
674 })?,
675 source_slice_count as usize,
676 recovery_count as usize,
677 forward_memory_limit,
678 cpu_memory,
679 )?;
680 let volumes = allocate_volumes(
681 options.first_exponent,
682 recovery_count,
683 options.volume_count,
684 options.volume_scheme,
685 &stem_name,
686 sources
687 .iter()
688 .map(|source| source.file_length)
689 .max()
690 .unwrap_or(0),
691 block_size,
692 )?;
693 let volume_paths = volumes
694 .iter()
695 .map(|volume| output_parent.join(&volume.filename))
696 .collect::<Vec<_>>();
697 let mut output_paths = Vec::with_capacity(volume_paths.len() + 1);
698 output_paths.push(main_path.clone());
699 output_paths.extend(volume_paths.iter().cloned());
700 let target_snapshots = validate_output_targets(&output_paths, &sources, options.overwrite)?;
701
702 let mut main_body = Vec::with_capacity(12 + sources.len() * 16);
703 main_body.extend_from_slice(&block_size.to_le_bytes());
704 main_body.extend_from_slice(&(sources.len() as u32).to_le_bytes());
705 for source in &sources {
706 main_body.extend_from_slice(source.file_id.as_bytes());
707 }
708 let recovery_set_id = RecoverySetId::from_bytes(md5(&main_body));
709 let recovery_exponents = (0..recovery_count)
710 .map(|offset| options.first_exponent + offset)
711 .collect();
712 let memory = memory_plan_for(
713 &sources,
714 input_lengths.len(),
715 source_slice_count,
716 block_size,
717 MemoryPlanPaths {
718 base_path: &base_path,
719 output_stem: &output_stem,
720 main_path: &main_path,
721 output_paths: &output_paths,
722 },
723 &volumes,
724 forward_memory_limit,
725 forward_memory,
726 )?;
727
728 Ok(Par2CreatePlan {
729 base_path,
730 output_stem,
731 main_path,
732 volumes,
733 volume_scheme: options.volume_scheme,
734 volume_paths,
735 output_paths,
736 target_snapshots,
737 sources,
738 slice_size: block_size,
739 source_slice_count,
740 recovery_count,
741 first_exponent: options.first_exponent,
742 recovery_exponents,
743 recovery_set_id,
744 forward_kernel: options.forward_kernel,
745 backend: options.backend,
746 memory,
747 dry_run: options.dry_run,
748 skipped_empty,
749 })
750}
751
752fn canonical_directory(path: &Path, label: &str) -> Result<PathBuf> {
753 let canonical = fs::canonicalize(path).map_err(|error| Par2Error::UnsafeCreationOutput {
754 path: path.display().to_string(),
755 reason: format!("{label} cannot be resolved: {error}"),
756 })?;
757 if !fs::metadata(&canonical).map_err(Par2Error::Io)?.is_dir() {
758 return Err(Par2Error::UnsafeCreationOutput {
759 path: path.display().to_string(),
760 reason: format!("{label} is not a directory"),
761 });
762 }
763 Ok(canonical)
764}
765
766fn canonical_source_directory(path: &Path) -> Result<PathBuf> {
767 let canonical = fs::canonicalize(path).map_err(|error| Par2Error::UnsafeCreationSource {
768 path: path.display().to_string(),
769 reason: format!("base path cannot be resolved: {error}"),
770 })?;
771 if !fs::metadata(&canonical).map_err(Par2Error::Io)?.is_dir() {
772 return Err(Par2Error::UnsafeCreationSource {
773 path: path.display().to_string(),
774 reason: "base path is not a directory".to_string(),
775 });
776 }
777 Ok(canonical)
778}
779
780fn normalize_output(output: &Path) -> Result<(PathBuf, PathBuf, PathBuf, String)> {
781 let file_name = output
782 .file_name()
783 .and_then(|name| name.to_str())
784 .ok_or_else(|| Par2Error::UnsafeCreationOutput {
785 path: output.display().to_string(),
786 reason: "output must have a valid UTF-8 filename".to_string(),
787 })?;
788 if file_name.is_empty() || file_name == "." || file_name == ".." || file_name.contains('\0') {
789 return Err(Par2Error::UnsafeCreationOutput {
790 path: output.display().to_string(),
791 reason: "output filename is empty or unsafe".to_string(),
792 });
793 }
794 let parent = output
795 .parent()
796 .filter(|path| !path.as_os_str().is_empty())
797 .unwrap_or_else(|| Path::new("."));
798 let parent = canonical_directory(parent, "output directory")?;
799 let (stem_name, main_name) = match file_name.rsplit_once('.') {
800 Some((stem, extension)) if extension.eq_ignore_ascii_case("par2") => {
801 if stem.is_empty() {
802 return Err(Par2Error::UnsafeCreationOutput {
803 path: output.display().to_string(),
804 reason: "output stem is empty".to_string(),
805 });
806 }
807 (stem.to_string(), file_name.to_string())
808 }
809 _ => (file_name.to_string(), format!("{file_name}.par2")),
810 };
811 let stem_path = parent.join(&stem_name);
812 let main_path = parent.join(main_name);
813 Ok((parent, stem_path, main_path, stem_name))
814}
815
816pub(crate) fn validate_output_targets(
817 output_paths: &[PathBuf],
818 sources: &[CreationSource],
819 overwrite: bool,
820) -> Result<Vec<TargetSnapshot>> {
821 let mut snapshots = Vec::with_capacity(output_paths.len());
822 for (index, target) in output_paths.iter().enumerate() {
823 if output_paths[..index].iter().any(|other| other == target) {
824 return Err(Par2Error::UnsafeCreationOutput {
825 path: target.display().to_string(),
826 reason: "output paths are not unique".to_string(),
827 });
828 }
829 if sources.iter().any(|source| source.path == *target) {
830 return Err(Par2Error::UnsafeCreationOutput {
831 path: target.display().to_string(),
832 reason: "output would replace an explicit source file".to_string(),
833 });
834 }
835 let snapshot = capture_target_snapshot(target).map_err(Par2Error::Io)?;
836 match snapshot {
837 TargetSnapshot::Directory => {
838 return Err(Par2Error::UnsafeCreationOutput {
839 path: target.display().to_string(),
840 reason: "output path is a directory".to_string(),
841 });
842 }
843 TargetSnapshot::Symlink => {
844 return Err(Par2Error::UnsafeCreationOutput {
845 path: target.display().to_string(),
846 reason: "output path is a symlink".to_string(),
847 });
848 }
849 TargetSnapshot::Special => {
850 return Err(Par2Error::UnsafeCreationOutput {
851 path: target.display().to_string(),
852 reason: "output path is not a regular file".to_string(),
853 });
854 }
855 TargetSnapshot::File(_) if !overwrite => {
856 return Err(Par2Error::CreationOutputExists {
857 path: target.display().to_string(),
858 });
859 }
860 TargetSnapshot::Absent | TargetSnapshot::File(_) => {}
861 }
862 snapshots.push(snapshot);
863 }
864 Ok(snapshots)
865}
866
867fn choose_block_size(lengths: &[InputLength], sizing: BlockSizing) -> Result<u64> {
868 match sizing {
869 BlockSizing::Bytes(bytes) => validate_block_size(bytes),
870 BlockSizing::Count(count) => {
871 if count == 0 {
872 return Err(Par2Error::InvalidCreationOptions {
873 reason: "block count must be greater than zero".to_string(),
874 });
875 }
876 if (count as usize) < lengths.len() {
877 return Err(Par2Error::InvalidCreationOptions {
878 reason: format!(
879 "block count {count} is smaller than the source file count {}",
880 lengths.len()
881 ),
882 });
883 }
884 if count as usize == lengths.len() {
885 return largest_rounded_block_size(lengths);
886 }
887 smallest_block_for_count(lengths, count as u64)
888 }
889 BlockSizing::Auto => automatic_block_size(lengths),
890 }
891}
892
893fn automatic_block_size(lengths: &[InputLength]) -> Result<u64> {
894 let total_bytes = lengths.iter().try_fold(0u64, |total, input| {
895 total
896 .checked_add(input.length)
897 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
898 reason: "source byte count overflows".to_string(),
899 })
900 })?;
901 let target = total_bytes
902 .checked_add(AUTO_SOURCE_SLICE_TARGET - 1)
903 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
904 reason: "automatic block-size rounding overflows".to_string(),
905 })?
906 / AUTO_SOURCE_SLICE_TARGET;
907 let minimum =
908 total_bytes
909 .checked_add(32_768 - 1)
910 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
911 reason: "automatic block-size limit rounding overflows".to_string(),
912 })?
913 / 32_768;
914 round_block_size(target.max(minimum).max(4))
915}
916
917fn largest_rounded_block_size(lengths: &[InputLength]) -> Result<u64> {
918 let largest = lengths.iter().map(|input| input.length).max().unwrap_or(0);
919 round_block_size(largest.max(4))
920}
921
922fn round_block_size(bytes: u64) -> Result<u64> {
923 bytes
924 .checked_add(3)
925 .map(|value| value / 4 * 4)
926 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
927 reason: "block size rounding overflows".to_string(),
928 })
929}
930
931fn validate_block_size(block_size: u64) -> Result<u64> {
932 if block_size == 0 || !block_size.is_multiple_of(4) {
933 return Err(Par2Error::InvalidCreationOptions {
934 reason: format!("block size {block_size} is not a positive multiple of four"),
935 });
936 }
937 Ok(block_size)
938}
939
940fn smallest_block_for_count(lengths: &[InputLength], target: u64) -> Result<u64> {
941 let maximum = lengths.iter().try_fold(4u64, |maximum, input| {
942 let rounded =
943 input
944 .length
945 .checked_add(3)
946 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
947 reason: "source length rounding overflows".to_string(),
948 })?
949 / 4
950 * 4;
951 Ok::<u64, Par2Error>(maximum.max(rounded))
952 })?;
953 let mut low = 4u64;
954 let mut high = maximum;
955 while low < high {
956 let low_units = low / 4;
957 let high_units = high / 4;
958 let mid = (low_units + (high_units - low_units) / 2) * 4;
959 if count_for_block(lengths, mid)? <= target {
960 high = mid;
961 } else {
962 low = mid + 4;
963 }
964 }
965 Ok(low)
966}
967
968fn count_for_block(lengths: &[InputLength], block_size: u64) -> Result<u64> {
969 lengths.iter().try_fold(0u64, |total, input| {
970 let count = if input.length == 0 {
971 0
972 } else {
973 (input.length - 1) / block_size + 1
974 };
975 total
976 .checked_add(count)
977 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
978 reason: "source slice count overflows".to_string(),
979 })
980 })
981}
982
983fn choose_recovery_count(
984 source_slice_count: u32,
985 amount: RecoveryAmount,
986 first_exponent: RecoveryExponent,
987) -> Result<u32> {
988 match amount {
989 RecoveryAmount::Count(count) => {
990 if count > 32_768 {
991 return Err(Par2Error::InvalidCreationOptions {
992 reason: "explicit recovery count cannot exceed 32768".to_string(),
993 });
994 }
995 Ok(count)
996 }
997 RecoveryAmount::Percent(percent) => {
998 let scaled = (source_slice_count as u64)
999 .checked_mul(percent as u64)
1000 .and_then(|value| value.checked_add(50))
1001 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
1002 reason: "percentage recovery count overflows".to_string(),
1003 })?;
1004 let mut count = scaled / 100;
1005 if percent > 0 && count == 0 {
1006 count = 1;
1007 }
1008 let count = u32::try_from(count).map_err(|_| Par2Error::InvalidCreationOptions {
1009 reason: "percentage recovery count exceeds the exponent range".to_string(),
1010 })?;
1011 validate_exponent_range(first_exponent, count)?;
1012 Ok(count)
1013 }
1014 }
1015}
1016
1017fn validate_exponent_range(first_exponent: RecoveryExponent, count: u32) -> Result<()> {
1018 if first_exponent > 32_768 {
1019 return Err(Par2Error::InvalidCreationOptions {
1020 reason: "first recovery exponent cannot exceed 32768".to_string(),
1021 });
1022 }
1023 let end =
1024 first_exponent
1025 .checked_add(count)
1026 .ok_or_else(|| Par2Error::InvalidCreationOptions {
1027 reason: "recovery exponent range overflows".to_string(),
1028 })?;
1029 if end > MAX_RECOVERY_EXPONENT {
1030 return Err(Par2Error::InvalidCreationOptions {
1031 reason: "first recovery exponent plus count must be less than 65536".to_string(),
1032 });
1033 }
1034 Ok(())
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040
1041 #[test]
1042 fn memory_default_policy_has_floor_and_32_bit_cap() {
1043 assert_eq!(memory_limit_for(None, 64), MEMORY_FLOOR_BYTES,);
1044 assert_eq!(
1045 memory_limit_for(Some((MEMORY_FLOOR_BYTES as u64) * 16), 64),
1046 MEMORY_FLOOR_BYTES * 2,
1047 );
1048 assert_eq!(
1049 memory_limit_for(Some(u64::MAX), 32),
1050 MEMORY_32_BIT_CAP_BYTES,
1051 );
1052 }
1053
1054 #[cfg(target_os = "windows")]
1055 #[test]
1056 fn windows_global_memory_status_reports_physical_memory() {
1057 assert!(physical_memory_bytes().is_some_and(|bytes| bytes > 0));
1058 }
1059}