Skip to main content

par2_rs/create/
mod.rs

1//! Validated PAR2 creation with deterministic packet allocation and transactional outputs.
2//! Output transactions detect ordinary replacement races, but assume no other process with
3//! equivalent filesystem permissions mutates their staging or backup paths.
4
5pub mod options;
6pub mod source;
7pub mod volume;
8
9mod encode;
10mod metal;
11mod output;
12mod plan;
13mod transform;
14
15pub use encode::ForwardKernel;
16/// The process-stable worker width, under a name that says why the repairer
17/// wants it: sizing rayon's global pool on wasm (see
18/// [`reedsolomon_rs::threading::ensure_pool`]). Repair has no width knob of its
19/// own, and this value is already the crate's single embedder-supplied answer
20/// to "how wide is the host", so both sides agree by construction.
21pub(crate) use encode::configured_create_threads as configured_create_threads_for_pool;
22pub use options::{BlockSizing, CreationBackend, Par2CreatorOptions, RecoveryAmount, VolumeScheme};
23pub use output::Par2CreateOutcome;
24pub use plan::{Par2CreatePlan, Par2MemoryPlan};
25pub use source::CreationSource;
26pub use volume::RecoveryVolumePlan;
27
28use crate::error::{Par2Error, Result};
29
30use self::output::write_outputs;
31use self::plan::build_plan_with_cache;
32
33/// High-level PAR2 creator.
34#[derive(Clone)]
35pub struct Par2Creator {
36    options: Par2CreatorOptions,
37    /// Source scan shared by this creator's `plan()` and `create()` calls, so
38    /// one creation reads and hashes its inputs once rather than once per
39    /// plan build. See [`self::source::SourceScanCache`] for what a reused
40    /// entry still re-validates. Clones share it: a clone is the same creator
41    /// over the same inputs, not a second opinion about them.
42    scan: std::sync::Arc<self::source::SourceScanCache>,
43}
44
45impl Par2Creator {
46    /// Construct a creator from explicit source and output options.
47    pub fn new(options: Par2CreatorOptions) -> Self {
48        Self {
49            options,
50            scan: std::sync::Arc::new(self::source::SourceScanCache::new()),
51        }
52    }
53
54    /// Borrow the options used by this creator.
55    pub fn options(&self) -> &Par2CreatorOptions {
56        &self.options
57    }
58
59    /// Validate inputs, hash sources, and allocate packets and output volumes.
60    pub fn plan(&self) -> Result<Par2CreatePlan> {
61        build_plan_with_cache(&self.options, Some(&self.scan))
62    }
63
64    /// Create the outputs described by a validated plan.
65    pub fn create(&self, plan: &Par2CreatePlan) -> Result<Par2CreateOutcome> {
66        if self.options.cancellation.is_cancelled() {
67            return Err(Par2Error::Cancelled);
68        }
69        // No-op on native (rayon's default sizing is already right). On
70        // `wasm32-wasip1-threads` this is what actually gives the banded
71        // accumulation, parallel source hashing, and parallel volume
72        // validation a pool wider than one worker — the guest cannot read the
73        // host core count, so the width comes from the same process-stable
74        // value the band shape uses. Placed on the execution entry point, never
75        // on `plan()`, so plan-only callers still never spawn a pool.
76        reedsolomon_rs::threading::ensure_pool(self::encode::configured_create_threads);
77        plan.validate_integrity()?;
78        self::plan::validate_output_targets(
79            &plan.output_paths,
80            &plan.sources,
81            self.options.overwrite,
82        )?;
83        // Rebuilt from the current inputs, exactly as before: every path is
84        // resolved and stat'ed again and every derived quantity recomputed.
85        // What the memo removes is the second READ of bytes whose fingerprint
86        // has not moved since `plan()` produced them.
87        let canonical = self::plan::build_plan_with_cache(&self.options, Some(&self.scan))?;
88        if plan != &canonical {
89            return Err(Par2Error::InvalidCreationOptions {
90                reason: "creation plan differs from creator options or current inputs".to_string(),
91            });
92        }
93        let slice_size =
94            usize::try_from(plan.slice_size).map_err(|_| Par2Error::ResourceLimitExceeded {
95                reason: "slice size exceeds addressable memory".to_string(),
96            })?;
97        let selected = metal::select_backend(
98            self.options.backend,
99            slice_size,
100            plan.source_slice_count as usize,
101            plan.recovery_count as usize,
102            self.options.memory_limit,
103        )?;
104        let selected_backend = metal::selected_policy(&selected);
105        if self.options.dry_run {
106            return Ok(Par2CreateOutcome {
107                recovery_set_id: plan.recovery_set_id,
108                main_path: plan.main_path.clone(),
109                volume_paths: plan.volume_paths.clone(),
110                output_paths: plan.output_paths.clone(),
111                source_slice_count: plan.source_slice_count,
112                recovery_count: plan.recovery_count,
113                bytes_written: 0,
114                dry_run: true,
115                requested_backend: self.options.backend,
116                selected_backend,
117            });
118        }
119
120        write_outputs(plan, canonical.sources, &self.options, selected)
121    }
122}