1#[cfg(any(target_os = "linux", target_os = "macos"))]
2use std::ffi::CString;
3use std::fs::{self, File, OpenOptions};
4use std::io::{Read, Seek, SeekFrom, Write};
5#[cfg(windows)]
6use std::mem::MaybeUninit;
7use std::mem::size_of;
8#[cfg(any(target_os = "linux", target_os = "macos"))]
9use std::os::unix::ffi::OsStrExt;
10use std::path::{Path, PathBuf};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use crate::checksum::Md5State;
14use crate::error::{Par2Error, Result};
15use crate::packet::encode::{encode_header, encode_packet, start_streamed_hash};
16use crate::packet::header::{HEADER_SIZE, PacketHeader, TYPE_CREATOR};
17use crate::packet::{Packet, scan_packets_from_path_with_set_ids_cancellable};
18use crate::types::{CancellationToken, FileId, RecoveryExponent, RecoverySetId};
19
20use super::encode::{EncodeAttempt, ForwardEncoder, ForwardEncoderOptions, ForwardRecoverySink};
21use super::metal::{SelectedBackend, selected_policy};
22use super::options::{CreationBackend, Par2CreatorOptions};
23use super::plan::Par2CreatePlan;
24use super::source::{CreationSource, DiskSourceProvider};
25use super::volume::RecoveryVolumePlan;
26
27const CREATOR_ID: &[u8] = b"par2-rs";
28static ZERO_WRITE_BUFFER: [u8; 64 * 1024] = [0; 64 * 1024];
29
30#[cfg(test)]
31static CANCEL_AFTER_VALIDATION_SCAN: std::sync::atomic::AtomicBool =
32 std::sync::atomic::AtomicBool::new(false);
33
34#[cfg(test)]
35fn validation_checkpoint(cancellation: &CancellationToken) {
36 use std::sync::atomic::Ordering;
37
38 if CANCEL_AFTER_VALIDATION_SCAN.swap(false, Ordering::Relaxed) {
39 cancellation.cancel();
40 }
41}
42
43#[cfg(not(test))]
44fn validation_checkpoint(_: &CancellationToken) {}
45
46pub(crate) fn estimate_critical_packet_bytes(sources: &[CreationSource]) -> Result<usize> {
47 let main_body = 12usize
48 .checked_add(sources.len().checked_mul(16).ok_or_else(|| {
49 Par2Error::ResourceLimitExceeded {
50 reason: "Main packet memory estimate overflows".to_string(),
51 }
52 })?)
53 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
54 reason: "Main packet memory estimate overflows".to_string(),
55 })?;
56 let mut total = encoded_packet_len(main_body)?;
57 for source in sources {
58 let description_body = 56usize.checked_add(source.par2_name.len()).ok_or_else(|| {
59 Par2Error::ResourceLimitExceeded {
60 reason: "FileDesc memory estimate overflows".to_string(),
61 }
62 })?;
63 let checksum_body = 16usize
64 .checked_add(
65 source
66 .slice_checksums
67 .len()
68 .checked_mul(20)
69 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
70 reason: "IFSC memory estimate overflows".to_string(),
71 })?,
72 )
73 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
74 reason: "IFSC memory estimate overflows".to_string(),
75 })?;
76 total = checked_memory_add(
77 total,
78 encoded_packet_len(description_body)?,
79 "critical packet memory estimate overflows",
80 )?;
81 total = checked_memory_add(
82 total,
83 encoded_packet_len(checksum_body)?,
84 "critical packet memory estimate overflows",
85 )?;
86 }
87 Ok(total)
88}
89
90pub(crate) fn estimate_packet_build_workspace_bytes(sources: &[CreationSource]) -> Result<usize> {
91 let mut largest_body = 0usize;
92 for source in sources {
93 let description_body = 56usize
94 .checked_add(source.par2_name.len())
95 .and_then(|length| length.checked_add(3))
96 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
97 reason: "FileDesc packet workspace estimate overflows".to_string(),
98 })?;
99 let checksum_body = 16usize
100 .checked_add(
101 source
102 .slice_checksums
103 .len()
104 .checked_mul(20)
105 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
106 reason: "IFSC packet workspace estimate overflows".to_string(),
107 })?,
108 )
109 .and_then(|length| length.checked_add(3))
110 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
111 reason: "IFSC packet workspace estimate overflows".to_string(),
112 })?;
113 largest_body = largest_body.max(description_body).max(checksum_body);
114 }
115 checked_memory_add(
116 size_of::<Vec<u8>>(),
117 largest_body,
118 "critical packet workspace estimate overflows",
119 )
120}
121
122pub(crate) fn estimate_transaction_workspace_bytes(
123 base_path: &Path,
124 output_stem: &Path,
125 main_path: &Path,
126 output_paths: &[PathBuf],
127 volumes: &[RecoveryVolumePlan],
128 sources: &[CreationSource],
129 recovery_count: u32,
130) -> Result<usize> {
131 let critical_count = 1usize
132 .checked_add(sources.len().checked_mul(2).ok_or_else(|| {
133 Par2Error::ResourceLimitExceeded {
134 reason: "critical packet controller estimate overflows".to_string(),
135 }
136 })?)
137 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
138 reason: "critical packet controller estimate overflows".to_string(),
139 })?;
140 let mut total = checked_memory_product(
141 critical_count,
142 size_of::<(ExpectedPacket, Vec<u8>)>(),
143 "critical packet controller estimate overflows",
144 )?;
145 total = checked_memory_add(
146 total,
147 encoded_packet_len(CREATOR_ID.len())?,
148 "creation transaction estimate overflows",
149 )?;
150 total = checked_memory_add(
151 total,
152 checked_memory_product(
153 output_paths.len(),
154 size_of::<VolumeState>(),
155 "staged volume estimate overflows",
156 )?,
157 "creation transaction estimate overflows",
158 )?;
159 total = checked_memory_add(
160 total,
161 checked_memory_product(
162 recovery_count as usize,
163 size_of::<RecoveryLocation>(),
164 "recovery location estimate overflows",
165 )?,
166 "creation transaction estimate overflows",
167 )?;
168
169 for (index, volume) in volumes.iter().enumerate() {
170 let expected_count = expected_packet_count(volume.recovery_count, critical_count)?;
171 let expected_capacity = allocation_capacity_upper_bound(expected_count)?;
172 let slot_capacity = allocation_capacity_upper_bound(volume.recovery_count as usize)?;
173 total = checked_memory_add(
174 total,
175 checked_memory_product(
176 expected_capacity,
177 size_of::<ExpectedPacket>(),
178 "expected packet estimate overflows",
179 )?,
180 "creation transaction estimate overflows",
181 )?;
182 total = checked_memory_add(
183 total,
184 checked_memory_product(
185 slot_capacity,
186 size_of::<RecoverySlot>(),
187 "recovery slot estimate overflows",
188 )?,
189 "creation transaction estimate overflows",
190 )?;
191 let path = output_paths
192 .get(index)
193 .ok_or_else(|| Par2Error::InvalidCreationOptions {
194 reason: "creation plan volume paths are inconsistent".to_string(),
195 })?;
196 total = checked_memory_add(
197 total,
198 path_memory_bytes(path).checked_add(128).ok_or_else(|| {
199 Par2Error::ResourceLimitExceeded {
200 reason: "staged path estimate overflows".to_string(),
201 }
202 })?,
203 "creation transaction estimate overflows",
204 )?;
205 total = checked_memory_add(
206 total,
207 volume.filename.len().checked_add(64).ok_or_else(|| {
208 Par2Error::ResourceLimitExceeded {
209 reason: "volume filename estimate overflows".to_string(),
210 }
211 })?,
212 "creation transaction estimate overflows",
213 )?;
214 }
215
216 for bytes in [
217 checked_memory_product(sources.len(), 128, "source provider estimate overflows")?,
218 checked_memory_product(
219 recovery_count as usize,
220 size_of::<RecoveryExponent>(),
221 "encoder exponent estimate overflows",
222 )?,
223 checked_memory_product(
224 volumes.len(),
225 size_of::<RecoveryVolumePlan>(),
226 "volume plan estimate overflows",
227 )?,
228 checked_memory_product(
229 output_paths.len(),
230 size_of::<PathBuf>(),
231 "output path estimate overflows",
232 )?,
233 ] {
234 total = checked_memory_add(total, bytes, "creation transaction estimate overflows")?;
235 }
236 for path in output_paths {
237 total = checked_memory_add(
238 total,
239 path_memory_bytes(path).checked_add(128).ok_or_else(|| {
240 Par2Error::ResourceLimitExceeded {
241 reason: "output path estimate overflows".to_string(),
242 }
243 })?,
244 "output path estimate overflows",
245 )?;
246 }
247 for path in [base_path, output_stem, main_path] {
248 total = checked_memory_add(
249 total,
250 path_memory_bytes(path).checked_add(128).ok_or_else(|| {
251 Par2Error::ResourceLimitExceeded {
252 reason: "creation path estimate overflows".to_string(),
253 }
254 })?,
255 "creation path estimate overflows",
256 )?;
257 }
258 let cloned_path_capacity = allocation_capacity_upper_bound(output_paths.len())?;
259 for (count, element_size) in [
260 (cloned_path_capacity, size_of::<PathBuf>()),
261 (cloned_path_capacity, size_of::<TargetSnapshot>()),
262 (cloned_path_capacity, size_of::<BackupEntry>()),
263 (cloned_path_capacity, size_of::<InstalledTarget>()),
264 ] {
265 total = checked_memory_add(
266 total,
267 checked_memory_product(
268 count,
269 element_size,
270 "transaction metadata estimate overflows",
271 )?,
272 "creation transaction estimate overflows",
273 )?;
274 }
275 for path in output_paths {
276 let path_copy_bytes = path_memory_bytes(path).checked_add(256).ok_or_else(|| {
277 Par2Error::ResourceLimitExceeded {
278 reason: "transaction path capacity estimate overflows".to_string(),
279 }
280 })?;
281 total = checked_memory_add(
282 total,
283 checked_memory_product(
284 6,
285 path_copy_bytes,
286 "transaction path capacity estimate overflows",
287 )?,
288 "creation transaction estimate overflows",
289 )?;
290 }
291 total = checked_memory_add(
292 total,
293 size_of::<StagedOutputs>(),
294 "creation transaction estimate overflows",
295 )?;
296 Ok(total)
297}
298
299pub(crate) fn estimate_validation_workspace_bytes(
300 sources: &[CreationSource],
301 output_paths: &[PathBuf],
302 volumes: &[RecoveryVolumePlan],
303 critical_packet_bytes: usize,
304) -> Result<usize> {
305 const SCANNER_BUFFER_BYTES: usize = 256 * 1024;
306 const RECOVERY_HASH_BUFFER_BYTES: usize = 256 * 1024;
307 const MAX_CREATOR_BODY_BYTES: usize = 100_000;
308
309 let critical_count = 1usize
310 .checked_add(sources.len().checked_mul(2).ok_or_else(|| {
311 Par2Error::ResourceLimitExceeded {
312 reason: "validation critical packet count overflows".to_string(),
313 }
314 })?)
315 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
316 reason: "validation critical packet count overflows".to_string(),
317 })?;
318 let max_recovery_count = volumes
319 .iter()
320 .map(|volume| volume.recovery_count)
321 .max()
322 .unwrap_or(0);
323 let critical_copies = usize::try_from(bit_length(max_recovery_count).max(1)).map_err(|_| {
324 Par2Error::ResourceLimitExceeded {
325 reason: "validation critical copy count overflows".to_string(),
326 }
327 })?;
328 let max_packet_count = expected_packet_count(max_recovery_count, critical_count)?;
329 let parsed_critical_bytes = checked_memory_product(
330 critical_packet_bytes,
331 critical_copies,
332 "validation parsed critical packet estimate overflows",
333 )?;
334 let parsed_packet_slots = checked_memory_product(
335 allocation_capacity_upper_bound(max_packet_count)?,
336 size_of::<crate::packet::ScannedPacket>(),
337 "validation parsed packet controller estimate overflows",
338 )?;
339 let recovery_path_bytes = output_paths
340 .iter()
341 .map(|path| path_memory_bytes(path).saturating_add(128))
342 .max()
343 .unwrap_or(128);
344 let parsed_recovery_paths = checked_memory_product(
345 max_packet_count,
346 recovery_path_bytes,
347 "validation recovery path estimate overflows",
348 )?;
349 let per_volume = [
350 parsed_critical_bytes,
351 parsed_packet_slots,
352 parsed_recovery_paths,
353 size_of::<Vec<crate::packet::ScannedPacket>>(),
354 MAX_CREATOR_BODY_BYTES
355 .checked_add(size_of::<crate::packet::Packet>())
356 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
357 reason: "validation Creator packet estimate overflows".to_string(),
358 })?,
359 SCANNER_BUFFER_BYTES,
360 RECOVERY_HASH_BUFFER_BYTES,
361 ]
362 .into_iter()
363 .try_fold(0usize, |total, bytes| {
364 checked_memory_add(total, bytes, "validation workspace estimate overflows")
365 })?;
366 let staged_volumes = output_paths.len();
375 let threads = super::encode::configured_create_threads();
376 let concurrent_volumes = if threads == 1 || staged_volumes <= 1 {
377 1
378 } else {
379 staged_volumes.min(threads.saturating_add(1))
380 };
381 checked_memory_product(
382 per_volume,
383 concurrent_volumes,
384 "concurrent validation workspace estimate overflows",
385 )
386}
387
388fn expected_packet_count(recovery_count: u32, critical_count: usize) -> Result<usize> {
389 let recovery_count_usize = recovery_count as usize;
390 if recovery_count == 0 {
391 return critical_count
392 .checked_add(1)
393 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
394 reason: "expected packet estimate overflows".to_string(),
395 });
396 }
397 let copies = bit_length(recovery_count) as usize;
398 let mut pending = 0u64;
399 let mut total =
400 recovery_count_usize
401 .checked_add(1)
402 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
403 reason: "expected packet estimate overflows".to_string(),
404 })?;
405 for _ in 0..recovery_count {
406 pending = pending
407 .checked_add(
408 (copies as u64)
409 .checked_mul(critical_count as u64)
410 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
411 reason: "expected packet estimate overflows".to_string(),
412 })?,
413 )
414 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
415 reason: "expected packet estimate overflows".to_string(),
416 })?;
417 while pending >= recovery_count as u64 {
418 total = total
419 .checked_add(1)
420 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
421 reason: "expected packet estimate overflows".to_string(),
422 })?;
423 pending -= recovery_count as u64;
424 }
425 }
426 Ok(total)
427}
428
429fn allocation_capacity_upper_bound(length: usize) -> Result<usize> {
430 if length == 0 {
431 Ok(0)
432 } else {
433 length
434 .checked_next_power_of_two()
435 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
436 reason: "transaction allocation estimate overflows".to_string(),
437 })
438 }
439}
440
441fn checked_memory_product(left: usize, right: usize, reason: &'static str) -> Result<usize> {
442 left.checked_mul(right)
443 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
444 reason: reason.to_string(),
445 })
446}
447
448fn checked_memory_add(left: usize, right: usize, reason: &'static str) -> Result<usize> {
449 left.checked_add(right)
450 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
451 reason: reason.to_string(),
452 })
453}
454
455fn encoded_packet_len(body_len: usize) -> Result<usize> {
456 let padded = body_len
457 .checked_add(3)
458 .map(|length| length / 4 * 4)
459 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
460 reason: "packet memory estimate overflows".to_string(),
461 })?;
462 HEADER_SIZE
463 .checked_add(padded)
464 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
465 reason: "packet memory estimate overflows".to_string(),
466 })
467}
468
469fn path_memory_bytes(path: &Path) -> usize {
470 path.as_os_str().len()
471}
472
473#[derive(Debug, Clone)]
475pub struct Par2CreateOutcome {
476 pub recovery_set_id: RecoverySetId,
478 pub main_path: PathBuf,
480 pub volume_paths: Vec<PathBuf>,
482 pub output_paths: Vec<PathBuf>,
484 pub source_slice_count: u32,
486 pub recovery_count: u32,
488 pub bytes_written: u64,
490 pub dry_run: bool,
492 pub requested_backend: CreationBackend,
494 pub selected_backend: CreationBackend,
496}
497
498#[derive(Clone)]
499enum ExpectedPacket {
500 Main,
501 FileDescription(FileId),
502 InputFileSliceChecksum(FileId),
503 Recovery(RecoveryExponent),
504 Creator,
505}
506
507struct RecoverySlot {
508 exponent: RecoveryExponent,
509 header_offset: u64,
510 data_offset: u64,
511 bytes_written: u64,
512 hasher: Md5State,
513}
514
515struct VolumeState {
516 stage_path: PathBuf,
517 target_path: PathBuf,
518 file: File,
519 expected: Vec<ExpectedPacket>,
520 recovery_slots: Vec<RecoverySlot>,
521}
522
523#[derive(Clone, Copy)]
524struct RecoveryLocation {
525 volume_index: usize,
526 slot_index: usize,
527}
528
529#[derive(Clone, Copy)]
538struct CriticalLocation {
539 volume_index: usize,
540 offset: u64,
541 critical_index: usize,
542 len: usize,
543}
544
545struct StagedOutputs {
546 volumes: Vec<VolumeState>,
547 locations: Vec<RecoveryLocation>,
548 critical_offsets: Vec<CriticalLocation>,
550 planned_targets: Vec<TargetSnapshot>,
551 planned_pins: Vec<Option<InodePin>>,
561 committed: bool,
562}
563
564#[derive(Debug)]
623struct InodePin {
624 #[cfg(any(unix, windows))]
625 handle: File,
626}
627
628impl InodePin {
629 #[cfg(any(unix, windows))]
639 fn open(path: &Path, set_size: usize) -> Result<Self> {
640 match File::open(path) {
641 Ok(handle) => Ok(Self { handle }),
642 Err(error) => Err(open_pin_error(path, set_size, &error)),
643 }
644 }
645
646 #[cfg(any(unix, windows))]
654 fn adopt(handle: File) -> Self {
655 Self { handle }
656 }
657
658 #[cfg(unix)]
664 fn still_at(&self, path: &Path) -> std::io::Result<bool> {
665 use std::os::unix::fs::MetadataExt;
666
667 let pinned = self.handle.metadata()?;
668 match fs::symlink_metadata(path) {
669 Ok(current) => Ok(current.is_file()
670 && current.dev() == pinned.dev()
671 && current.ino() == pinned.ino()),
672 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
673 Err(error) => Err(error),
674 }
675 }
676
677 #[cfg(windows)]
692 fn still_at(&self, path: &Path) -> std::io::Result<bool> {
693 let pinned = FileIdentity::from_file(&self.handle)?;
694 Ok(target_file_identity(path)? == Some(pinned))
695 }
696
697 #[cfg(not(any(unix, windows)))]
705 fn open(_path: &Path, _set_size: usize) -> Result<Self> {
706 Ok(Self {})
707 }
708
709 #[cfg(not(any(unix, windows)))]
713 fn adopt(_handle: File) -> Self {
714 Self {}
715 }
716
717 #[cfg(not(any(unix, windows)))]
718 fn still_at(&self, _path: &Path) -> std::io::Result<bool> {
719 Ok(true)
720 }
721}
722
723#[cfg(any(unix, windows))]
728fn open_pin_error(path: &Path, set_size: usize, error: &std::io::Error) -> Par2Error {
729 let limit = open_file_limit_description();
730 Par2Error::CreationValidation {
731 path: path.display().to_string(),
732 reason: format!(
733 "cannot pin output for the transaction ({error}); \
734 the transaction owns {set_size} files and needs one open descriptor per file, {limit}"
735 ),
736 }
737}
738
739#[cfg(unix)]
740fn open_file_limit_description() -> String {
741 match open_file_limits() {
742 Some((soft, hard)) => {
743 format!("RLIMIT_NOFILE soft={soft} hard={hard}")
744 }
745 None => "RLIMIT_NOFILE could not be read".to_string(),
746 }
747}
748
749#[cfg(windows)]
754fn open_file_limit_description() -> String {
755 "and Windows imposes no per-process descriptor limit at this scale, so this is a \
756 sharing or access denial rather than exhaustion"
757 .to_string()
758}
759
760#[cfg(unix)]
761fn open_file_limits() -> Option<(u64, u64)> {
762 unsafe {
765 let mut limit: libc::rlimit = std::mem::zeroed();
766 if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) != 0 {
767 return None;
768 }
769 Some((limit.rlim_cur as u64, limit.rlim_max as u64))
770 }
771}
772
773#[cfg(unix)]
782fn raise_open_file_limit() {
783 static RAISED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
784 RAISED.get_or_init(|| {
785 unsafe {
788 let mut limit: libc::rlimit = std::mem::zeroed();
789 if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) != 0 {
790 return;
791 }
792 if limit.rlim_cur >= limit.rlim_max {
793 return;
794 }
795 let hard = limit.rlim_max;
801 let start = limit.rlim_cur;
802 for candidate in [hard, 262_144, 65_536, 10_240, 4_096, 1_024] {
803 if candidate <= start {
804 break;
805 }
806 let candidate = candidate.min(hard);
807 let mut attempt = limit;
808 attempt.rlim_cur = candidate;
809 if libc::setrlimit(libc::RLIMIT_NOFILE, &attempt) == 0 {
810 return;
811 }
812 }
813 }
814 });
815}
816
817#[cfg(not(unix))]
821fn raise_open_file_limit() {}
822
823struct InstalledTarget {
824 path: PathBuf,
825 identity: Option<FileIdentity>,
826 pin: Option<InodePin>,
831}
832
833struct BackupEntry {
834 target: PathBuf,
835 backup: PathBuf,
836 namespace: PathBuf,
837 identity: FileIdentity,
838 pin: InodePin,
841}
842
843#[derive(Debug)]
844enum PublishError {
845 NotInstalled(std::io::Error),
846 Installed {
847 identity: FileIdentity,
848 error: std::io::Error,
849 },
850}
851
852impl PublishError {
853 #[cfg(test)]
854 fn into_io_error(self) -> std::io::Error {
855 match self {
856 Self::NotInstalled(error) | Self::Installed { error, .. } => error,
857 }
858 }
859}
860
861#[derive(Debug, PartialEq, Eq)]
862enum RollbackTarget {
863 Missing,
864 OwnedQuarantined(PathBuf),
865 Unchanged,
866}
867
868impl RollbackTarget {
869 fn can_restore_backup(&self) -> bool {
870 matches!(self, Self::Missing | Self::OwnedQuarantined(_))
871 }
872}
873
874#[derive(Clone, Copy, Debug, PartialEq, Eq)]
875pub(crate) enum FileIdentity {
876 #[cfg(unix)]
877 Unix {
878 device: u64,
879 inode: u64,
880 birth: Option<std::time::SystemTime>,
887 },
888 #[cfg(windows)]
889 Windows {
890 volume: u32,
891 index: u64,
892 birth: Option<u64>,
909 },
910 #[cfg(target_os = "wasi")]
919 Wasi {
920 device: u64,
921 inode: u64,
922 birth: Option<std::time::SystemTime>,
923 },
924}
925
926#[derive(Clone, Copy, Debug, PartialEq, Eq)]
927pub(crate) enum TargetSnapshot {
928 Absent,
929 File(FileIdentity),
930 Directory,
931 Symlink,
932 Special,
933}
934
935#[cfg(windows)]
936#[repr(C)]
937struct WindowsFileTime {
938 low_date_time: u32,
939 high_date_time: u32,
940}
941
942#[cfg(windows)]
946#[repr(C)]
947#[allow(dead_code)]
948struct WindowsByHandleFileInformation {
949 file_attributes: u32,
950 creation_time: WindowsFileTime,
951 last_access_time: WindowsFileTime,
952 last_write_time: WindowsFileTime,
953 volume_serial_number: u32,
954 file_size_high: u32,
955 file_size_low: u32,
956 number_of_links: u32,
957 file_index_high: u32,
958 file_index_low: u32,
959}
960
961#[cfg(windows)]
962#[allow(non_snake_case)]
963unsafe extern "system" {
964 fn GetFileInformationByHandle(
965 file: std::os::windows::io::RawHandle,
966 information: *mut WindowsByHandleFileInformation,
967 ) -> i32;
968}
969
970impl FileIdentity {
971 #[cfg(target_os = "wasi")]
978 fn from_wasi_path(path: &Path, metadata: &fs::Metadata) -> Option<Self> {
979 let raw = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).ok()?;
980 let stat = unsafe {
984 let mut stat: libc::stat = std::mem::zeroed();
985 if libc::lstat(raw.as_ptr(), &mut stat) != 0 {
986 return None;
987 }
988 stat
989 };
990 Some(Self::Wasi {
991 device: stat.st_dev as u64,
992 inode: stat.st_ino as u64,
993 birth: metadata.created().ok(),
994 })
995 }
996
997 #[cfg(unix)]
998 fn from_metadata(metadata: &fs::Metadata) -> Option<Self> {
999 use std::os::unix::fs::MetadataExt;
1000
1001 Some(Self::Unix {
1002 device: metadata.dev(),
1003 inode: metadata.ino(),
1004 birth: metadata.created().ok(),
1005 })
1006 }
1007
1008 #[cfg(windows)]
1009 fn from_file(file: &File) -> std::io::Result<Self> {
1010 use std::os::windows::io::AsRawHandle;
1011
1012 let mut information = MaybeUninit::<WindowsByHandleFileInformation>::uninit();
1013 let succeeded =
1014 unsafe { GetFileInformationByHandle(file.as_raw_handle(), information.as_mut_ptr()) }
1015 != 0;
1016 if !succeeded {
1017 return Err(std::io::Error::last_os_error());
1018 }
1019 let information = unsafe { information.assume_init() };
1020 let birth = (u64::from(information.creation_time.high_date_time) << 32)
1021 | u64::from(information.creation_time.low_date_time);
1022 Ok(Self::Windows {
1023 volume: information.volume_serial_number,
1024 index: (u64::from(information.file_index_high) << 32)
1025 | u64::from(information.file_index_low),
1026 birth: (birth != 0).then_some(birth),
1029 })
1030 }
1031}
1032
1033impl StagedOutputs {
1034 fn create(
1035 plan: &Par2CreatePlan,
1036 critical: &[(ExpectedPacket, Vec<u8>)],
1037 creator: &[u8],
1038 cancellation: &CancellationToken,
1039 ) -> Result<Self> {
1040 raise_open_file_limit();
1044 let planned_pins = plan
1048 .output_paths
1049 .iter()
1050 .zip(plan.target_snapshots.iter())
1051 .map(|(target, snapshot)| match snapshot {
1052 TargetSnapshot::Absent => Ok(None),
1053 _ => InodePin::open(target, plan.output_paths.len()).map(Some),
1054 })
1055 .collect::<Result<Vec<_>>>();
1056 let mut volumes: Vec<VolumeState> = Vec::with_capacity(plan.output_paths.len());
1057 for (index, target) in plan.output_paths.iter().enumerate() {
1058 if cancellation.is_cancelled() {
1059 cleanup_stage_files(volumes);
1060 return Err(Par2Error::Cancelled);
1061 }
1062 let (stage_path, file) = match create_stage_file(target, index) {
1063 Ok(result) => result,
1064 Err(error) => {
1065 cleanup_stage_files(volumes);
1066 return Err(error);
1067 }
1068 };
1069 volumes.push(VolumeState {
1070 stage_path,
1071 target_path: target.clone(),
1072 file,
1073 expected: Vec::new(),
1074 recovery_slots: Vec::new(),
1075 });
1076 }
1077 let mut staged = Self {
1078 volumes,
1079 locations: Vec::with_capacity(plan.recovery_count as usize),
1080 critical_offsets: Vec::new(),
1081 planned_targets: plan.target_snapshots.clone(),
1082 planned_pins: planned_pins?,
1083 committed: false,
1084 };
1085
1086 for critical_index in 0..critical.len() {
1087 check_cancel(cancellation)?;
1088 append_critical_packet(&mut staged, 0, critical_index, critical)?;
1089 }
1090 check_cancel(cancellation)?;
1091 append_packet(&mut staged.volumes[0], ExpectedPacket::Creator, creator)?;
1092
1093 for (volume_offset, volume_plan) in plan.volumes.iter().enumerate() {
1094 check_cancel(cancellation)?;
1095 let volume_index = volume_offset + 1;
1096 if volume_plan.recovery_count == 0 {
1097 for critical_index in 0..critical.len() {
1098 check_cancel(cancellation)?;
1099 append_critical_packet(&mut staged, volume_index, critical_index, critical)?;
1100 }
1101 } else {
1102 let copies = bit_length(volume_plan.recovery_count) as u64;
1103 let critical_count = critical.len() as u64;
1104 let mut packet_count = 0u64;
1105 let mut next_critical = 0usize;
1106 for offset in 0..volume_plan.recovery_count {
1107 check_cancel(cancellation)?;
1108 let exponent = volume_plan.first_exponent + offset;
1109 let global_index = staged.locations.len();
1110 append_recovery_slot(
1111 &mut staged,
1112 volume_index,
1113 global_index,
1114 exponent,
1115 plan,
1116 cancellation,
1117 )?;
1118 packet_count = packet_count
1119 .checked_add(copies.checked_mul(critical_count).ok_or_else(|| {
1120 Par2Error::ResourceLimitExceeded {
1121 reason: "critical packet interleaving count overflows".to_string(),
1122 }
1123 })?)
1124 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
1125 reason: "critical packet interleaving count overflows".to_string(),
1126 })?;
1127 while packet_count >= volume_plan.recovery_count as u64 {
1128 check_cancel(cancellation)?;
1129 append_critical_packet(&mut staged, volume_index, next_critical, critical)?;
1130 next_critical = (next_critical + 1) % critical.len();
1131 packet_count -= volume_plan.recovery_count as u64;
1132 }
1133 }
1134 }
1135 check_cancel(cancellation)?;
1136 append_packet(
1137 &mut staged.volumes[volume_index],
1138 ExpectedPacket::Creator,
1139 creator,
1140 )?;
1141 }
1142 Ok(staged)
1143 }
1144
1145 fn rewrite_critical_packets(
1154 &mut self,
1155 critical: &[(ExpectedPacket, Vec<u8>)],
1156 cancellation: &CancellationToken,
1157 ) -> Result<()> {
1158 for location in &self.critical_offsets {
1159 check_cancel(cancellation)?;
1160 let (_, packet) = critical.get(location.critical_index).ok_or_else(|| {
1161 Par2Error::CreationValidation {
1162 path: self.volumes[location.volume_index]
1163 .stage_path
1164 .display()
1165 .to_string(),
1166 reason: "critical packet set changed after staging".to_string(),
1167 }
1168 })?;
1169 let volume = &mut self.volumes[location.volume_index];
1170 if packet.len() != location.len {
1171 return Err(Par2Error::CreationValidation {
1172 path: volume.stage_path.display().to_string(),
1173 reason: "critical packet length changed after staging".to_string(),
1174 });
1175 }
1176 volume
1177 .file
1178 .seek(SeekFrom::Start(location.offset))
1179 .map_err(Par2Error::Io)?;
1180 volume.file.write_all(packet).map_err(Par2Error::Io)?;
1181 }
1182 for volume in &mut self.volumes {
1183 volume.file.flush().map_err(Par2Error::Io)?;
1184 }
1185 Ok(())
1186 }
1187
1188 fn finish_recovery_headers(
1189 &mut self,
1190 slice_size: usize,
1191 set_id: RecoverySetId,
1192 cancellation: &CancellationToken,
1193 ) -> Result<()> {
1194 for volume in &mut self.volumes {
1195 for slot in &mut volume.recovery_slots {
1196 check_cancel(cancellation)?;
1197 if slot.bytes_written != slice_size as u64 {
1198 return Err(Par2Error::CreationValidation {
1199 path: volume.stage_path.display().to_string(),
1200 reason: format!(
1201 "recovery exponent {} received {} of {} bytes",
1202 slot.exponent, slot.bytes_written, slice_size
1203 ),
1204 });
1205 }
1206 let packet_length = (HEADER_SIZE as u64)
1207 .checked_add(4)
1208 .and_then(|length| length.checked_add(slice_size as u64))
1209 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
1210 reason: "recovery packet length overflows".to_string(),
1211 })?;
1212 let hash = std::mem::replace(&mut slot.hasher, Md5State::new()).finalize();
1213 let header = encode_header(
1214 crate::packet::header::TYPE_RECOVERY,
1215 set_id,
1216 packet_length,
1217 hash,
1218 )?;
1219 volume.file.seek(SeekFrom::Start(slot.header_offset))?;
1220 volume.file.write_all(&header)?;
1221 }
1222 volume.file.flush()?;
1223 volume.file.sync_all()?;
1224 }
1225 Ok(())
1226 }
1227
1228 fn validate(
1229 &mut self,
1230 plan: &Par2CreatePlan,
1231 sources: &[CreationSource],
1232 cancellation: &CancellationToken,
1233 ) -> Result<()> {
1234 let validate_parallel = reedsolomon_rs::threading::parallel_enabled()
1242 && self.volumes.len() > 1
1243 && super::encode::configured_create_threads() != 1;
1244 if !validate_parallel {
1245 for volume in &mut self.volumes {
1246 check_cancel(cancellation)?;
1247 volume.file.flush()?;
1248 volume.file.sync_all()?;
1249 validate_staged_volume(volume, plan, sources, cancellation)?;
1250 }
1251 return Ok(());
1252 }
1253 use rayon::prelude::*;
1254 self.volumes
1255 .par_iter_mut()
1256 .map(|volume| {
1257 check_cancel(cancellation)?;
1258 volume.file.flush()?;
1259 volume.file.sync_all()?;
1260 validate_staged_volume(volume, plan, sources, cancellation)
1261 })
1262 .collect::<Vec<Result<()>>>()
1263 .into_iter()
1264 .collect::<Result<Vec<()>>>()
1265 .map(|_| ())
1266 }
1267
1268 fn commit(self, overwrite: bool, cancellation: &CancellationToken) -> Result<()> {
1269 self.commit_with_publish_hook(overwrite, cancellation, |_, _| {})
1270 }
1271
1272 fn commit_with_publish_hook<F>(
1273 self,
1274 overwrite: bool,
1275 cancellation: &CancellationToken,
1276 before_publish: F,
1277 ) -> Result<()>
1278 where
1279 F: FnMut(&Path, &Path),
1280 {
1281 self.commit_with_transaction_hooks(overwrite, cancellation, |_, _| {}, before_publish)
1282 }
1283
1284 fn commit_with_transaction_hooks<F, G>(
1285 mut self,
1286 overwrite: bool,
1287 cancellation: &CancellationToken,
1288 mut before_backup_rename: F,
1289 mut before_publish: G,
1290 ) -> Result<()>
1291 where
1292 F: FnMut(&Path, &Path),
1293 G: FnMut(&Path, &Path),
1294 {
1295 for volume in &mut self.volumes {
1296 volume.file.flush()?;
1297 volume.file.sync_all()?;
1298 }
1299 let volumes = std::mem::take(&mut self.volumes);
1300 let stage_paths = volumes
1301 .iter()
1302 .map(|volume| volume.stage_path.clone())
1303 .collect::<Vec<_>>();
1304 let targets = volumes
1305 .iter()
1306 .map(|volume| volume.target_path.clone())
1307 .collect::<Vec<_>>();
1308 let mut stage_handles = volumes
1313 .into_iter()
1314 .map(|volume| Some(volume.file))
1315 .collect::<Vec<_>>();
1316 let planned_targets = std::mem::take(&mut self.planned_targets);
1317 let planned_pins = std::mem::take(&mut self.planned_pins);
1318
1319 let mut backups = Vec::<BackupEntry>::new();
1320 let mut installed = Vec::<InstalledTarget>::new();
1321 let result = (|| {
1322 if planned_targets.len() != targets.len() {
1323 return Err(Par2Error::InvalidCreationOptions {
1324 reason: "creation target snapshot count is inconsistent".to_string(),
1325 });
1326 }
1327 if overwrite {
1328 for (index, target) in targets.iter().enumerate() {
1329 if cancellation.is_cancelled() {
1330 return Err(Par2Error::Cancelled);
1331 }
1332 let planned = planned_targets[index];
1333 let current = capture_target_snapshot(target).map_err(Par2Error::Io)?;
1334 let pinned_object_intact =
1338 match planned_pins.get(index).and_then(Option::as_ref) {
1339 Some(pin) => pin.still_at(target).map_err(Par2Error::Io)?,
1340 None => true,
1341 };
1342 if current != planned || !pinned_object_intact {
1343 return Err(Par2Error::CreationValidation {
1344 path: target.display().to_string(),
1345 reason: "output target changed after planning".to_string(),
1346 });
1347 }
1348 if matches!(planned, TargetSnapshot::Directory) {
1349 return Err(Par2Error::UnsafeCreationOutput {
1350 path: target.display().to_string(),
1351 reason: "output path is a directory".to_string(),
1352 });
1353 }
1354 if matches!(planned, TargetSnapshot::Absent) {
1355 continue;
1356 }
1357 let TargetSnapshot::File(_) = planned else {
1358 return Err(Par2Error::UnsafeCreationOutput {
1359 path: target.display().to_string(),
1360 reason: "output path is not a regular file".to_string(),
1361 });
1362 };
1363 let pin = InodePin::open(target, targets.len())?;
1367 let (namespace, backup) = reserve_backup_namespace(target, index)?;
1368 before_backup_rename(target, &backup);
1369 if let Err(error) = fs::rename(target, &backup) {
1370 let _ = fs::remove_dir(&namespace);
1371 return Err(Par2Error::Io(error));
1372 }
1373 let moved = capture_target_snapshot(&backup).map_err(Par2Error::Io)?;
1374 if moved != planned {
1375 match restore_moved_target(&backup, target, moved) {
1376 Ok(true) => {
1377 fs::remove_dir(&namespace).map_err(|error| {
1378 backup_recovery_error(
1379 target,
1380 &backup,
1381 format!("output backup cleanup failed: {error}"),
1382 )
1383 })?;
1384 }
1385 Ok(false) => {
1386 return Err(backup_recovery_error(
1387 target,
1388 &backup,
1389 "output restore found an occupied target".to_string(),
1390 ));
1391 }
1392 Err(error) => {
1393 return Err(backup_recovery_error(
1394 target,
1395 &backup,
1396 format!("output restore failed: {error}"),
1397 ));
1398 }
1399 }
1400 return Err(Par2Error::CreationValidation {
1401 path: target.display().to_string(),
1402 reason: "output target changed while reserving backup".to_string(),
1403 });
1404 }
1405 let TargetSnapshot::File(identity) = moved else {
1406 return Err(Par2Error::CreationValidation {
1407 path: target.display().to_string(),
1408 reason: "output identity is unavailable".to_string(),
1409 });
1410 };
1411 backups.push(BackupEntry {
1412 target: target.clone(),
1413 backup,
1414 namespace,
1415 identity,
1416 pin,
1417 });
1418 }
1419 }
1420 for (index, (stage, target)) in stage_paths.iter().zip(targets.iter()).enumerate() {
1421 if cancellation.is_cancelled() {
1422 return Err(Par2Error::Cancelled);
1423 }
1424 let pin = stage_handles
1425 .get_mut(index)
1426 .and_then(|slot| slot.take())
1427 .map(InodePin::adopt);
1428 let identity = match publish_no_replace_with_tracking(stage, target, || {
1429 before_publish(stage, target)
1430 }) {
1431 Ok(identity) => identity,
1432 Err(PublishError::Installed { identity, error }) => {
1433 installed.push(InstalledTarget {
1434 path: target.clone(),
1435 identity: Some(identity),
1436 pin,
1437 });
1438 return Err(Par2Error::Io(error));
1439 }
1440 Err(PublishError::NotInstalled(error)) => {
1441 if error.kind() == std::io::ErrorKind::AlreadyExists {
1442 return Err(Par2Error::CreationOutputExists {
1443 path: target.display().to_string(),
1444 });
1445 }
1446 return Err(Par2Error::Io(error));
1447 }
1448 };
1449 installed.push(InstalledTarget {
1450 path: target.clone(),
1451 identity: Some(identity),
1452 pin,
1453 });
1454 }
1455 sync_parent_directories(&targets)?;
1456 Ok(())
1457 })();
1458
1459 if let Err(error) = result {
1460 let mut restorable_targets = Vec::new();
1461 let mut rollback_error = None;
1462 for installed_target in installed.iter().rev() {
1463 match quarantine_owned_target(installed_target) {
1464 Ok(rollback) => {
1465 if rollback.can_restore_backup() {
1466 restorable_targets.push(installed_target.path.clone());
1467 }
1468 }
1469 Err(error) => {
1470 if rollback_error.is_none() {
1471 rollback_error = Some(error);
1472 }
1473 }
1474 }
1475 }
1476 if overwrite {
1477 for backup_entry in backups.iter().rev() {
1478 if !installed
1479 .iter()
1480 .any(|installed_target| installed_target.path == backup_entry.target)
1481 || restorable_targets
1482 .iter()
1483 .any(|restorable| restorable == &backup_entry.target)
1484 {
1485 let backup_is_ours = backup_entry
1492 .pin
1493 .still_at(&backup_entry.backup)
1494 .unwrap_or(false);
1495 let identity = target_file_identity(&backup_entry.backup);
1496 if !backup_is_ours
1497 || !matches!(identity, Ok(Some(identity)) if identity == backup_entry.identity)
1498 {
1499 if rollback_error.is_none() {
1500 rollback_error = Some(match identity {
1501 Err(error) => Par2Error::Io(error),
1502 Ok(_) => Par2Error::CreationValidation {
1503 path: backup_entry.target.display().to_string(),
1504 reason: "backup identity changed during restore"
1505 .to_string(),
1506 },
1507 });
1508 }
1509 continue;
1510 }
1511 match restore_moved_target(
1512 &backup_entry.backup,
1513 &backup_entry.target,
1514 TargetSnapshot::File(backup_entry.identity),
1515 ) {
1516 Ok(true) => {}
1517 Ok(false) => {
1518 if rollback_error.is_none() {
1519 rollback_error = Some(backup_recovery_error(
1520 &backup_entry.target,
1521 &backup_entry.backup,
1522 "output restore lost a replacement race".to_string(),
1523 ));
1524 }
1525 }
1526 Err(error) => {
1527 if rollback_error.is_none() {
1528 rollback_error = Some(backup_recovery_error(
1529 &backup_entry.target,
1530 &backup_entry.backup,
1531 format!("output restore failed: {error}"),
1532 ));
1533 }
1534 }
1535 }
1536 }
1537 }
1538 }
1539 for backup_entry in &backups {
1540 if !backup_entry.backup.exists() {
1541 let _ = fs::remove_dir(&backup_entry.namespace);
1542 }
1543 }
1544 for stage in &stage_paths {
1545 let _ = fs::remove_file(stage);
1546 }
1547 return Err(rollback_error.unwrap_or(error));
1548 }
1549 let mut cleanup_error = None;
1550 for backup_entry in backups {
1551 match target_file_identity(&backup_entry.backup) {
1552 Ok(Some(identity)) if identity == backup_entry.identity => {
1553 if let Err(error) = fs::remove_file(&backup_entry.backup) {
1554 if cleanup_error.is_none() {
1555 cleanup_error = Some(backup_recovery_error(
1556 &backup_entry.target,
1557 &backup_entry.backup,
1558 format!("output backup cleanup failed: {error}"),
1559 ));
1560 }
1561 } else if let Err(error) = fs::remove_dir(&backup_entry.namespace)
1562 && cleanup_error.is_none()
1563 {
1564 cleanup_error = Some(backup_recovery_error(
1565 &backup_entry.target,
1566 &backup_entry.namespace,
1567 format!("output backup namespace cleanup failed: {error}"),
1568 ));
1569 }
1570 }
1571 Ok(_) => {
1572 if cleanup_error.is_none() {
1573 cleanup_error = Some(backup_recovery_error(
1574 &backup_entry.target,
1575 &backup_entry.backup,
1576 "output backup changed before cleanup".to_string(),
1577 ));
1578 }
1579 }
1580 Err(error) => {
1581 if cleanup_error.is_none() {
1582 cleanup_error = Some(backup_recovery_error(
1583 &backup_entry.target,
1584 &backup_entry.backup,
1585 format!("output backup cleanup could not be verified: {error}"),
1586 ));
1587 }
1588 }
1589 }
1590 }
1591 self.committed = true;
1592 cleanup_error.map_or(Ok(()), Err)
1593 }
1594
1595 fn bytes_written(&self) -> Result<u64> {
1596 self.volumes.iter().try_fold(0u64, |total, volume| {
1597 fs::metadata(&volume.stage_path)
1598 .map(|metadata| metadata.len())
1599 .or_else(|_| fs::metadata(&volume.target_path).map(|metadata| metadata.len()))
1600 .map_err(Par2Error::Io)
1601 .and_then(|length| {
1602 total
1603 .checked_add(length)
1604 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
1605 reason: "output byte count overflows".to_string(),
1606 })
1607 })
1608 })
1609 }
1610}
1611
1612impl Drop for StagedOutputs {
1613 fn drop(&mut self) {
1614 if self.committed {
1615 return;
1616 }
1617 cleanup_stage_files(std::mem::take(&mut self.volumes));
1618 }
1619}
1620
1621fn cleanup_stage_files(volumes: Vec<VolumeState>) {
1622 let stage_paths = volumes
1623 .iter()
1624 .map(|volume| volume.stage_path.clone())
1625 .collect::<Vec<_>>();
1626 drop(volumes);
1627 for stage_path in stage_paths {
1628 let _ = fs::remove_file(stage_path);
1629 }
1630}
1631
1632pub(crate) fn write_outputs(
1633 plan: &Par2CreatePlan,
1634 mut sources: Vec<CreationSource>,
1635 options: &Par2CreatorOptions,
1636 mut backend: SelectedBackend,
1637) -> Result<Par2CreateOutcome> {
1638 if options.cancellation.is_cancelled() {
1639 return Err(Par2Error::Cancelled);
1640 }
1641 let mut transform_policy = super::transform::policy_from_env();
1647 let mut hydrated = false;
1648 let mut staged = loop {
1649 let fuse_source_hashing = plan.recovery_count > 0
1657 && matches!(backend, SelectedBackend::Cpu)
1658 && super::encode::forward_stripe_count(
1659 plan.slice_size,
1660 plan.source_slice_count as usize,
1661 &plan.recovery_exponents,
1662 options
1663 .memory_limit
1664 .unwrap_or_else(super::plan::default_memory_limit),
1665 options.forward_kernel,
1666 transform_policy,
1667 )? == 1;
1668 if !fuse_source_hashing && !hydrated {
1669 super::source::hydrate_source_hashes(
1670 &mut sources,
1671 plan.slice_size,
1672 &options.cancellation,
1673 )?;
1674 hydrated = true;
1675 }
1676 let critical = build_critical_packets(plan, &sources)?;
1677 let creator = encode_creator_packet(plan.recovery_set_id)?;
1678 let mut staged = StagedOutputs::create(plan, &critical, &creator, &options.cancellation)?;
1679
1680 if plan.recovery_count > 0 {
1681 let slice_size =
1682 usize::try_from(plan.slice_size).map_err(|_| Par2Error::ResourceLimitExceeded {
1683 reason: "slice size exceeds addressable memory".to_string(),
1684 })?;
1685 let mut provider =
1686 DiskSourceProvider::open(&sources, slice_size, &options.cancellation)?;
1687 let attempt = {
1688 let mut sink = RecoveryWriter {
1689 outputs: &mut staged,
1690 cancellation: &options.cancellation,
1691 slice_size,
1692 };
1693 match &mut backend {
1694 SelectedBackend::Cpu => {
1695 let encoder =
1696 ForwardEncoder::new(slice_size, plan.recovery_exponents.clone())?;
1697 let encoder_options = ForwardEncoderOptions {
1698 memory_limit: options.memory_limit,
1699 cancel: Some(options.cancellation.clone()),
1700 progress: options.progress.clone(),
1701 kernel: options.forward_kernel,
1702 transform: transform_policy,
1703 };
1704 if fuse_source_hashing {
1705 let mut hasher =
1706 super::source::FusedSourceHasher::new(&sources, plan.slice_size)?;
1707 let attempt = encoder.encode_attempt(
1708 &mut provider,
1709 &encoder_options,
1710 &mut sink,
1711 Some(&mut hasher),
1712 )?;
1713 match attempt {
1714 EncodeAttempt::Complete => (attempt, Some(hasher.finish()?)),
1715 EncodeAttempt::TransformProbeMismatch => (attempt, None),
1716 }
1717 } else {
1718 let attempt = encoder.encode_attempt(
1719 &mut provider,
1720 &encoder_options,
1721 &mut sink,
1722 None,
1723 )?;
1724 (attempt, None)
1725 }
1726 }
1727 #[cfg(all(feature = "metal", target_os = "macos", target_arch = "aarch64"))]
1728 SelectedBackend::Metal(state) => {
1729 state.encode(
1730 &mut provider,
1731 &plan.recovery_exponents,
1732 slice_size,
1733 &options.cancellation,
1734 options.progress.clone(),
1735 &mut sink,
1736 )?;
1737 (EncodeAttempt::Complete, None)
1738 }
1739 }
1740 };
1741 let (attempt, fused) = attempt;
1742 if attempt == EncodeAttempt::TransformProbeMismatch {
1743 drop(provider);
1744 drop(staged);
1745 tracing::warn!(
1746 "PAR2 transform recovery arithmetic disagreed with the dense definition; \
1747 recreating the recovery volumes from the dense encoder"
1748 );
1749 transform_policy = super::transform::TransformPolicy::Never;
1750 continue;
1751 }
1752 provider.verify_unchanged()?;
1753 if let Some(fused) = fused {
1754 fused.apply(&mut sources)?;
1755 let critical = build_critical_packets(plan, &sources)?;
1759 staged.rewrite_critical_packets(&critical, &options.cancellation)?;
1760 }
1761 staged.finish_recovery_headers(
1762 slice_size,
1763 plan.recovery_set_id,
1764 &options.cancellation,
1765 )?;
1766 }
1767 break staged;
1768 };
1769
1770 staged.validate(plan, &sources, &options.cancellation)?;
1771 let bytes_written = staged.bytes_written()?;
1772 staged.commit(options.overwrite, &options.cancellation)?;
1773 Ok(Par2CreateOutcome {
1774 recovery_set_id: plan.recovery_set_id,
1775 main_path: plan.main_path.clone(),
1776 volume_paths: plan.volume_paths.clone(),
1777 output_paths: plan.output_paths.clone(),
1778 source_slice_count: plan.source_slice_count,
1779 recovery_count: plan.recovery_count,
1780 bytes_written,
1781 dry_run: false,
1782 requested_backend: options.backend,
1783 selected_backend: selected_policy(&backend),
1784 })
1785}
1786
1787fn build_critical_packets(
1788 plan: &Par2CreatePlan,
1789 sources: &[CreationSource],
1790) -> Result<Vec<(ExpectedPacket, Vec<u8>)>> {
1791 let mut main_body = Vec::with_capacity(12 + sources.len() * 16);
1792 main_body.extend_from_slice(&plan.slice_size.to_le_bytes());
1793 main_body.extend_from_slice(&(sources.len() as u32).to_le_bytes());
1794 for source in sources {
1795 main_body.extend_from_slice(source.file_id.as_bytes());
1796 }
1797 let mut packets = Vec::with_capacity(1 + sources.len() * 2);
1798 packets.push((
1799 ExpectedPacket::Main,
1800 encode_packet(
1801 crate::packet::header::TYPE_MAIN,
1802 plan.recovery_set_id,
1803 &main_body,
1804 )?,
1805 ));
1806 for source in sources {
1807 let mut body = Vec::with_capacity(56 + source.par2_name.len() + 3);
1808 body.extend_from_slice(source.file_id.as_bytes());
1809 body.extend_from_slice(&source.hash_full);
1810 body.extend_from_slice(&source.hash_16k);
1811 body.extend_from_slice(&source.file_length.to_le_bytes());
1812 body.extend_from_slice(source.par2_name.as_bytes());
1813 pad_body(&mut body)?;
1814 packets.push((
1815 ExpectedPacket::FileDescription(source.file_id),
1816 encode_packet(
1817 crate::packet::header::TYPE_FILE_DESC,
1818 plan.recovery_set_id,
1819 &body,
1820 )?,
1821 ));
1822 }
1823 for source in sources {
1824 let mut body = Vec::with_capacity(16 + source.slice_checksums.len() * 20);
1825 body.extend_from_slice(source.file_id.as_bytes());
1826 for checksum in &source.slice_checksums {
1827 body.extend_from_slice(&checksum.md5);
1828 body.extend_from_slice(&checksum.crc32.to_le_bytes());
1829 }
1830 pad_body(&mut body)?;
1831 packets.push((
1832 ExpectedPacket::InputFileSliceChecksum(source.file_id),
1833 encode_packet(
1834 crate::packet::header::TYPE_IFSC,
1835 plan.recovery_set_id,
1836 &body,
1837 )?,
1838 ));
1839 }
1840 Ok(packets)
1841}
1842
1843fn encode_creator_packet(set_id: RecoverySetId) -> Result<Vec<u8>> {
1844 let mut body = CREATOR_ID.to_vec();
1845 pad_body(&mut body)?;
1846 encode_packet(TYPE_CREATOR, set_id, &body)
1847}
1848
1849fn append_critical_packet(
1851 staged: &mut StagedOutputs,
1852 volume_index: usize,
1853 critical_index: usize,
1854 critical: &[(ExpectedPacket, Vec<u8>)],
1855) -> Result<()> {
1856 let (expected, packet) = &critical[critical_index];
1857 let volume =
1858 staged
1859 .volumes
1860 .get_mut(volume_index)
1861 .ok_or_else(|| Par2Error::CreationValidation {
1862 path: "critical packet output".to_string(),
1863 reason: "critical packet volume index is out of range".to_string(),
1864 })?;
1865 let offset = volume.file.stream_position().map_err(Par2Error::Io)?;
1866 append_packet(volume, expected.clone(), packet)?;
1867 staged.critical_offsets.push(CriticalLocation {
1868 volume_index,
1869 offset,
1870 critical_index,
1871 len: packet.len(),
1872 });
1873 Ok(())
1874}
1875
1876fn append_packet(volume: &mut VolumeState, expected: ExpectedPacket, packet: &[u8]) -> Result<()> {
1877 volume.file.write_all(packet).map_err(Par2Error::Io)?;
1878 volume.expected.push(expected);
1879 Ok(())
1880}
1881
1882fn append_recovery_slot(
1883 staged: &mut StagedOutputs,
1884 volume_index: usize,
1885 global_index: usize,
1886 exponent: RecoveryExponent,
1887 plan: &Par2CreatePlan,
1888 cancellation: &CancellationToken,
1889) -> Result<()> {
1890 check_cancel(cancellation)?;
1891 let slice_size =
1892 usize::try_from(plan.slice_size).map_err(|_| Par2Error::ResourceLimitExceeded {
1893 reason: "slice size exceeds addressable memory".to_string(),
1894 })?;
1895 let packet_length = (HEADER_SIZE as u64)
1896 .checked_add(4)
1897 .and_then(|length| length.checked_add(slice_size as u64))
1898 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
1899 reason: "recovery packet length overflows".to_string(),
1900 })?;
1901 let volume =
1902 staged
1903 .volumes
1904 .get_mut(volume_index)
1905 .ok_or_else(|| Par2Error::CreationValidation {
1906 path: plan.output_paths.get(volume_index).map_or_else(
1907 || "recovery output".to_string(),
1908 |path| path.display().to_string(),
1909 ),
1910 reason: "recovery volume index is out of range".to_string(),
1911 })?;
1912 let header_offset = volume.file.stream_position().map_err(Par2Error::Io)?;
1913 let header = encode_header(
1914 crate::packet::header::TYPE_RECOVERY,
1915 plan.recovery_set_id,
1916 packet_length,
1917 [0; 16],
1918 )?;
1919 volume.file.write_all(&header).map_err(Par2Error::Io)?;
1920 volume
1921 .file
1922 .write_all(&exponent.to_le_bytes())
1923 .map_err(Par2Error::Io)?;
1924 let data_offset = volume.file.stream_position().map_err(Par2Error::Io)?;
1925 write_zeroes(&mut volume.file, slice_size, cancellation)?;
1926 check_cancel(cancellation)?;
1927 volume.expected.push(ExpectedPacket::Recovery(exponent));
1928 let slot_index = volume.recovery_slots.len();
1929 volume.recovery_slots.push(RecoverySlot {
1930 exponent,
1931 header_offset,
1932 data_offset,
1933 bytes_written: 0,
1934 hasher: {
1935 let mut hasher =
1936 start_streamed_hash(plan.recovery_set_id, crate::packet::header::TYPE_RECOVERY);
1937 hasher.update(&exponent.to_le_bytes());
1938 hasher
1939 },
1940 });
1941 if global_index != staged.locations.len() {
1942 return Err(Par2Error::CreationValidation {
1943 path: volume.stage_path.display().to_string(),
1944 reason: "recovery output order is inconsistent".to_string(),
1945 });
1946 }
1947 staged.locations.push(RecoveryLocation {
1948 volume_index,
1949 slot_index,
1950 });
1951 Ok(())
1952}
1953
1954fn validate_staged_volume(
1955 volume: &VolumeState,
1956 plan: &Par2CreatePlan,
1957 sources: &[CreationSource],
1958 cancellation: &CancellationToken,
1959) -> Result<()> {
1960 check_cancel(cancellation)?;
1961 let file_length = fs::metadata(&volume.stage_path)
1962 .map_err(Par2Error::Io)?
1963 .len();
1964 let scan_ceiling = volume.expected.len().saturating_add(1);
1968 let scan_limits = crate::packet::PacketScanLimits::default()
1969 .with_max_retained_packets(scan_ceiling)
1970 .with_max_examined_packets(scan_ceiling as u64);
1971 let scanned = match scan_packets_from_path_with_set_ids_cancellable(
1972 &volume.stage_path,
1973 scan_limits,
1974 cancellation,
1975 ) {
1976 Ok(scanned) => scanned,
1977 Err(Par2Error::Cancelled) => return Err(Par2Error::Cancelled),
1978 Err(error) => {
1979 return Err(Par2Error::CreationValidation {
1980 path: volume.stage_path.display().to_string(),
1981 reason: error.to_string(),
1982 });
1983 }
1984 };
1985 validation_checkpoint(cancellation);
1986 check_cancel(cancellation)?;
1987 if scanned.len() != volume.expected.len() {
1988 return Err(validation_error(
1989 &volume.stage_path,
1990 format!(
1991 "parsed {} packets but staged {} packets",
1992 scanned.len(),
1993 volume.expected.len()
1994 ),
1995 ));
1996 }
1997 let mut file = File::open(&volume.stage_path).map_err(Par2Error::Io)?;
1998 let mut offset = 0u64;
1999 for (scanned, expected) in scanned.iter().zip(&volume.expected) {
2000 check_cancel(cancellation)?;
2001 if scanned.offset != offset || scanned.recovery_set_id != plan.recovery_set_id {
2002 return Err(validation_error(
2003 &volume.stage_path,
2004 "packet offsets or recovery-set identifiers are inconsistent".to_string(),
2005 ));
2006 }
2007 let mut header_bytes = [0u8; HEADER_SIZE];
2008 file.seek(SeekFrom::Start(offset)).map_err(Par2Error::Io)?;
2009 file.read_exact(&mut header_bytes).map_err(Par2Error::Io)?;
2010 let header = PacketHeader::parse(&header_bytes, offset)
2011 .map_err(|error| validation_error(&volume.stage_path, error.to_string()))?;
2012 offset = offset.checked_add(header.length).ok_or_else(|| {
2013 validation_error(&volume.stage_path, "packet offsets overflow".to_string())
2014 })?;
2015 validate_expected_packet(
2016 scanned,
2017 expected,
2018 plan,
2019 sources,
2020 &volume.stage_path,
2021 cancellation,
2022 )?;
2023 }
2024 check_cancel(cancellation)?;
2025 if offset != file_length {
2026 return Err(validation_error(
2027 &volume.stage_path,
2028 format!("packet stream ends at {offset}, file length is {file_length}"),
2029 ));
2030 }
2031 Ok(())
2032}
2033
2034fn validate_expected_packet(
2035 scanned: &crate::packet::ScannedPacket,
2036 expected: &ExpectedPacket,
2037 plan: &Par2CreatePlan,
2038 sources: &[CreationSource],
2039 path: &Path,
2040 cancellation: &CancellationToken,
2041) -> Result<()> {
2042 check_cancel(cancellation)?;
2043 if scanned.recovery_set_id != plan.recovery_set_id {
2044 return Err(validation_error(
2045 path,
2046 "packet recovery-set identifier differs from plan".to_string(),
2047 ));
2048 }
2049 match (expected, &scanned.packet) {
2050 (ExpectedPacket::Main, Packet::Main(packet)) => {
2051 let ids = sources
2052 .iter()
2053 .map(|source| source.file_id)
2054 .collect::<Vec<_>>();
2055 if packet.slice_size != plan.slice_size
2056 || packet.recovery_file_ids != ids
2057 || !packet.non_recovery_file_ids.is_empty()
2058 {
2059 return Err(validation_error(
2060 path,
2061 "Main packet content differs from plan".to_string(),
2062 ));
2063 }
2064 }
2065 (ExpectedPacket::FileDescription(file_id), Packet::FileDescription(packet)) => {
2066 let source = source_by_id(sources, *file_id).ok_or_else(|| {
2067 validation_error(
2068 path,
2069 "FileDesc packet references an unknown file".to_string(),
2070 )
2071 })?;
2072 if packet.file_id != source.file_id
2073 || packet.hash_full != source.hash_full
2074 || packet.hash_16k != source.hash_16k
2075 || packet.file_length != source.file_length
2076 || packet.par2_name != source.par2_name
2077 {
2078 return Err(validation_error(
2079 path,
2080 "FileDesc packet content differs from source".to_string(),
2081 ));
2082 }
2083 }
2084 (
2085 ExpectedPacket::InputFileSliceChecksum(file_id),
2086 Packet::InputFileSliceChecksum(packet),
2087 ) => {
2088 let source = source_by_id(sources, *file_id).ok_or_else(|| {
2089 validation_error(path, "IFSC packet references an unknown file".to_string())
2090 })?;
2091 if packet.file_id != source.file_id || packet.checksums != source.slice_checksums {
2092 return Err(validation_error(
2093 path,
2094 "IFSC packet content differs from source".to_string(),
2095 ));
2096 }
2097 }
2098 (ExpectedPacket::Recovery(exponent), Packet::RecoverySlice(packet)) => {
2099 if packet.exponent != *exponent || packet.data.len() != plan.slice_size as usize {
2100 return Err(validation_error(
2101 path,
2102 "recovery packet length or exponent differs from plan".to_string(),
2103 ));
2104 }
2105 let valid = match packet.data.validate_packet_hash_cancellable(
2106 plan.recovery_set_id.as_bytes(),
2107 *exponent,
2108 cancellation,
2109 ) {
2110 Ok(valid) => valid,
2111 Err(Par2Error::Cancelled) => return Err(Par2Error::Cancelled),
2112 Err(error) => return Err(validation_error(path, error.to_string())),
2113 };
2114 if !valid {
2115 return Err(validation_error(
2116 path,
2117 "recovery packet hash is invalid".to_string(),
2118 ));
2119 }
2120 }
2121 (ExpectedPacket::Creator, Packet::Creator(packet)) => {
2122 if packet.creator_id.as_bytes() != CREATOR_ID {
2123 return Err(validation_error(
2124 path,
2125 "Creator packet identifier differs from plan".to_string(),
2126 ));
2127 }
2128 }
2129 _ => {
2130 return Err(validation_error(
2131 path,
2132 "packet type differs from expected critical order".to_string(),
2133 ));
2134 }
2135 }
2136 Ok(())
2137}
2138
2139fn source_by_id(sources: &[CreationSource], file_id: FileId) -> Option<&CreationSource> {
2140 sources.iter().find(|source| source.file_id == file_id)
2141}
2142
2143struct RecoveryWriter<'a> {
2144 outputs: &'a mut StagedOutputs,
2145 cancellation: &'a CancellationToken,
2146 slice_size: usize,
2147}
2148
2149impl ForwardRecoverySink for RecoveryWriter<'_> {
2150 fn write_recovery_chunk(
2151 &mut self,
2152 output_index: usize,
2153 exponent: RecoveryExponent,
2154 offset: u64,
2155 data: &[u8],
2156 ) -> Result<()> {
2157 if self.cancellation.is_cancelled() {
2158 return Err(Par2Error::Cancelled);
2159 }
2160 if data.is_empty() {
2161 return Ok(());
2162 }
2163 if data.len() > self.slice_size
2164 || usize::try_from(offset)
2165 .ok()
2166 .and_then(|start| start.checked_add(data.len()))
2167 .is_none_or(|end| end > self.slice_size)
2168 {
2169 return Err(Par2Error::CreationValidation {
2170 path: "recovery output".to_string(),
2171 reason: "encoder supplied a chunk outside the slice".to_string(),
2172 });
2173 }
2174 let location = *self.outputs.locations.get(output_index).ok_or_else(|| {
2175 Par2Error::CreationValidation {
2176 path: "recovery output".to_string(),
2177 reason: "encoder supplied an unknown output index".to_string(),
2178 }
2179 })?;
2180 let volume = self
2181 .outputs
2182 .volumes
2183 .get_mut(location.volume_index)
2184 .ok_or_else(|| Par2Error::CreationValidation {
2185 path: "recovery output".to_string(),
2186 reason: "recovery volume index is out of range".to_string(),
2187 })?;
2188 let slot = volume
2189 .recovery_slots
2190 .get_mut(location.slot_index)
2191 .ok_or_else(|| Par2Error::CreationValidation {
2192 path: volume.stage_path.display().to_string(),
2193 reason: "recovery slot index is out of range".to_string(),
2194 })?;
2195 if slot.exponent != exponent || slot.bytes_written != offset {
2196 return Err(Par2Error::CreationValidation {
2197 path: volume.stage_path.display().to_string(),
2198 reason: "encoder recovery chunks are out of order".to_string(),
2199 });
2200 }
2201 let write_offset = slot.data_offset.checked_add(offset).ok_or_else(|| {
2202 Par2Error::ResourceLimitExceeded {
2203 reason: "recovery write offset overflows".to_string(),
2204 }
2205 })?;
2206 volume.file.seek(SeekFrom::Start(write_offset))?;
2207 volume.file.write_all(data)?;
2208 slot.hasher.update(data);
2209 slot.bytes_written = slot
2210 .bytes_written
2211 .checked_add(data.len() as u64)
2212 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
2213 reason: "recovery byte count overflows".to_string(),
2214 })?;
2215 Ok(())
2216 }
2217}
2218
2219fn sync_parent_directories(paths: &[PathBuf]) -> Result<()> {
2220 #[cfg(unix)]
2221 {
2222 let mut synced = Vec::<PathBuf>::new();
2223 for path in paths {
2224 let parent = path.parent().unwrap_or_else(|| Path::new("."));
2225 if synced.iter().any(|previous| previous == parent) {
2226 continue;
2227 }
2228 File::open(parent).map_err(Par2Error::Io)?.sync_all()?;
2229 synced.push(parent.to_path_buf());
2230 }
2231 }
2232 #[cfg(not(unix))]
2233 {
2234 let _ = paths;
2235 }
2236 Ok(())
2237}
2238
2239fn target_file_identity(path: &Path) -> std::io::Result<Option<FileIdentity>> {
2240 let metadata = match fs::symlink_metadata(path) {
2241 Ok(metadata) => metadata,
2242 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
2243 Err(error) => return Err(error),
2244 };
2245 if !metadata.is_file() {
2246 return Ok(None);
2247 }
2248 #[cfg(unix)]
2249 {
2250 Ok(FileIdentity::from_metadata(&metadata))
2251 }
2252 #[cfg(windows)]
2253 {
2254 match File::open(path) {
2255 Ok(file) => match fs::symlink_metadata(path) {
2256 Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => {
2257 FileIdentity::from_file(&file).map(Some)
2258 }
2259 Ok(_) => Ok(None),
2260 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
2261 Err(error) => Err(error),
2262 },
2263 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
2264 Err(error) => Err(error),
2265 }
2266 }
2267 #[cfg(target_os = "wasi")]
2268 {
2269 Ok(FileIdentity::from_wasi_path(path, &metadata))
2270 }
2271 #[cfg(not(any(unix, windows, target_os = "wasi")))]
2272 {
2273 let _ = path;
2274 Ok(None)
2275 }
2276}
2277
2278pub(crate) fn capture_target_snapshot(path: &Path) -> std::io::Result<TargetSnapshot> {
2279 match fs::symlink_metadata(path) {
2280 Ok(metadata) if metadata.file_type().is_symlink() => Ok(TargetSnapshot::Symlink),
2281 Ok(metadata) if metadata.is_dir() => Ok(TargetSnapshot::Directory),
2282 Ok(metadata) if metadata.is_file() => target_file_identity(path)?
2283 .map(TargetSnapshot::File)
2284 .ok_or_else(|| std::io::Error::other("output identity is unavailable")),
2285 Ok(_) => Ok(TargetSnapshot::Special),
2286 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(TargetSnapshot::Absent),
2287 Err(error) => Err(error),
2288 }
2289}
2290
2291fn quarantine_owned_target(target: &InstalledTarget) -> Result<RollbackTarget> {
2292 quarantine_owned_target_with_hook(target, || {})
2293}
2294
2295fn quarantine_owned_target_with_hook<F: FnOnce()>(
2296 target: &InstalledTarget,
2297 before_move: F,
2298) -> Result<RollbackTarget> {
2299 let Some(expected) = target.identity else {
2300 return Ok(RollbackTarget::Unchanged);
2301 };
2302 let (namespace, quarantine) = reserve_quarantine_namespace(&target.path, 0)?;
2303 let still_ours = match target.pin.as_ref() {
2307 Some(pin) => pin.still_at(&target.path).map_err(Par2Error::Io)?,
2308 None => matches!(
2309 target_file_identity(&target.path).map_err(Par2Error::Io)?,
2310 Some(identity) if identity == expected
2311 ),
2312 };
2313 if !still_ours {
2314 let _ = fs::remove_dir(&namespace);
2315 return match fs::symlink_metadata(&target.path) {
2316 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
2318 Ok(RollbackTarget::Missing)
2319 }
2320 Err(error) => Err(Par2Error::Io(error)),
2321 Ok(_) => Err(Par2Error::CreationValidation {
2323 path: target.path.display().to_string(),
2324 reason: "installed output identity changed before rollback".to_string(),
2325 }),
2326 };
2327 }
2328 before_move();
2329 match fs::rename(&target.path, &quarantine) {
2330 Ok(()) => {
2331 let moved_is_ours = match target.pin.as_ref() {
2336 Some(pin) => pin.still_at(&quarantine).map_err(Par2Error::Io)?,
2337 None => target_file_identity(&quarantine).map_err(Par2Error::Io)? == Some(expected),
2338 };
2339 if moved_is_ours {
2340 Ok(RollbackTarget::OwnedQuarantined(namespace))
2344 } else {
2345 let moved = capture_target_snapshot(&quarantine).map_err(Par2Error::Io)?;
2346 match restore_moved_target(&quarantine, &target.path, moved) {
2347 Ok(true) => {}
2348 Ok(false) => {
2349 return Err(backup_recovery_error(
2350 &target.path,
2351 &quarantine,
2352 "installed output restore found an occupied target".to_string(),
2353 ));
2354 }
2355 Err(error) => {
2356 return Err(backup_recovery_error(
2357 &target.path,
2358 &quarantine,
2359 format!("installed output restore failed: {error}"),
2360 ));
2361 }
2362 }
2363 fs::remove_dir(&namespace).map_err(|error| {
2364 backup_recovery_error(
2365 &target.path,
2366 &quarantine,
2367 format!("installed output quarantine cleanup failed: {error}"),
2368 )
2369 })?;
2370 Err(Par2Error::CreationValidation {
2371 path: target.path.display().to_string(),
2372 reason: "installed output identity changed during rollback".to_string(),
2373 })
2374 }
2375 }
2376 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
2377 let _ = fs::remove_dir(&namespace);
2378 Ok(RollbackTarget::Missing)
2379 }
2380 Err(error) => {
2381 let _ = fs::remove_dir(&namespace);
2382 Err(Par2Error::Io(error))
2383 }
2384 }
2385}
2386
2387fn backup_recovery_error(target: &Path, backup: &Path, reason: String) -> Par2Error {
2388 Par2Error::CreationValidation {
2389 path: target.display().to_string(),
2390 reason: format!("{reason}; recovery path: {}", backup.display()),
2391 }
2392}
2393
2394fn restore_moved_target(
2395 source: &Path,
2396 target: &Path,
2397 moved: TargetSnapshot,
2398) -> std::io::Result<bool> {
2399 if matches!(moved, TargetSnapshot::Absent) {
2400 return Ok(true);
2401 }
2402 match rename_no_replace(source, target) {
2403 Ok(()) => Ok(true),
2404 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(false),
2405 Err(error) if no_replace_unavailable(&error) => {
2406 if matches!(moved, TargetSnapshot::Directory) {
2407 return Err(error);
2408 }
2409 match fs::hard_link(source, target) {
2410 Ok(()) => match fs::remove_file(source) {
2411 Ok(()) => Ok(true),
2412 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(true),
2413 Err(error) => Err(error),
2414 },
2415 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(false),
2416 Err(error) => Err(error),
2417 }
2418 }
2419 Err(error) => Err(error),
2420 }
2421}
2422
2423fn no_replace_unavailable(error: &std::io::Error) -> bool {
2424 matches!(
2425 error.kind(),
2426 std::io::ErrorKind::Unsupported | std::io::ErrorKind::InvalidInput
2427 )
2428}
2429
2430#[cfg(target_os = "linux")]
2431fn rename_no_replace(source: &Path, target: &Path) -> std::io::Result<()> {
2432 let source = CString::new(source.as_os_str().as_bytes())
2433 .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))?;
2434 let target = CString::new(target.as_os_str().as_bytes())
2435 .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))?;
2436 let result = unsafe {
2442 libc::syscall(
2443 libc::SYS_renameat2,
2444 libc::AT_FDCWD,
2445 source.as_ptr(),
2446 libc::AT_FDCWD,
2447 target.as_ptr(),
2448 libc::RENAME_NOREPLACE,
2449 )
2450 };
2451 if result == 0 {
2452 Ok(())
2453 } else {
2454 Err(std::io::Error::last_os_error())
2455 }
2456}
2457
2458#[cfg(target_os = "macos")]
2459fn rename_no_replace(source: &Path, target: &Path) -> std::io::Result<()> {
2460 let source = CString::new(source.as_os_str().as_bytes())
2461 .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))?;
2462 let target = CString::new(target.as_os_str().as_bytes())
2463 .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))?;
2464 let result = unsafe {
2465 libc::renameatx_np(
2466 libc::AT_FDCWD,
2467 source.as_ptr(),
2468 libc::AT_FDCWD,
2469 target.as_ptr(),
2470 libc::RENAME_EXCL,
2471 )
2472 };
2473 if result == 0 {
2474 Ok(())
2475 } else {
2476 Err(std::io::Error::last_os_error())
2477 }
2478}
2479
2480#[cfg(windows)]
2481fn rename_no_replace(source: &Path, target: &Path) -> std::io::Result<()> {
2482 use std::os::windows::ffi::OsStrExt;
2483
2484 let source = source
2485 .as_os_str()
2486 .encode_wide()
2487 .chain(std::iter::once(0))
2488 .collect::<Vec<_>>();
2489 let target = target
2490 .as_os_str()
2491 .encode_wide()
2492 .chain(std::iter::once(0))
2493 .collect::<Vec<_>>();
2494 let result =
2495 unsafe { move_file_ex_w(source.as_ptr(), target.as_ptr(), MOVEFILE_WRITE_THROUGH) };
2496 if result != 0 {
2497 Ok(())
2498 } else {
2499 Err(std::io::Error::last_os_error())
2500 }
2501}
2502
2503#[cfg(windows)]
2504const MOVEFILE_WRITE_THROUGH: u32 = 0x0000_0008;
2505
2506#[cfg(windows)]
2507#[link(name = "kernel32")]
2508unsafe extern "system" {
2509 #[link_name = "MoveFileExW"]
2510 fn move_file_ex_w(source: *const u16, target: *const u16, flags: u32) -> i32;
2511}
2512
2513#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
2514fn rename_no_replace(_: &Path, _: &Path) -> std::io::Result<()> {
2515 Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
2516}
2517
2518#[cfg(not(any(unix, windows)))]
2519fn rename_no_replace(_: &Path, _: &Path) -> std::io::Result<()> {
2520 Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
2521}
2522
2523#[cfg(test)]
2524fn publish_no_replace(stage: &Path, target: &Path) -> std::io::Result<Option<FileIdentity>> {
2525 publish_no_replace_with_hook(stage, target, || {})
2526}
2527
2528#[cfg(test)]
2529fn publish_no_replace_with_hook<F: FnOnce()>(
2530 stage: &Path,
2531 target: &Path,
2532 before_publish: F,
2533) -> std::io::Result<Option<FileIdentity>> {
2534 publish_no_replace_with_tracking(stage, target, before_publish)
2535 .map(Some)
2536 .map_err(PublishError::into_io_error)
2537}
2538
2539fn publish_no_replace_with_tracking<F: FnOnce()>(
2540 stage: &Path,
2541 target: &Path,
2542 before_publish: F,
2543) -> std::result::Result<FileIdentity, PublishError> {
2544 publish_no_replace_with_tracking_hooks(stage, target, before_publish, || {})
2545}
2546
2547fn publish_no_replace_with_tracking_hooks<F: FnOnce(), G: FnOnce()>(
2548 stage: &Path,
2549 target: &Path,
2550 before_publish: F,
2551 after_link: G,
2552) -> std::result::Result<FileIdentity, PublishError> {
2553 let stage_identity = match target_file_identity(stage) {
2554 Ok(Some(identity)) => identity,
2555 Ok(None) => {
2556 return Err(PublishError::NotInstalled(std::io::Error::other(
2557 "staged output identity is unavailable",
2558 )));
2559 }
2560 Err(error) => return Err(PublishError::NotInstalled(error)),
2561 };
2562 before_publish();
2563 fs::hard_link(stage, target).map_err(PublishError::NotInstalled)?;
2568 after_link();
2569 if let Err(error) = fs::remove_file(stage) {
2570 return Err(PublishError::Installed {
2571 identity: stage_identity,
2572 error,
2573 });
2574 }
2575 Ok(stage_identity)
2576}
2577
2578fn process_token() -> u32 {
2593 #[cfg(not(target_family = "wasm"))]
2594 {
2595 std::process::id()
2596 }
2597 #[cfg(target_family = "wasm")]
2598 {
2599 static TOKEN: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
2600 *TOKEN.get_or_init(|| {
2601 SystemTime::now()
2602 .duration_since(UNIX_EPOCH)
2603 .map(|duration| duration.subsec_nanos())
2604 .unwrap_or(0)
2605 })
2606 }
2607}
2608
2609fn create_stage_file(target: &Path, index: usize) -> Result<(PathBuf, File)> {
2610 let parent = target.parent().unwrap_or_else(|| Path::new("."));
2611 let stamp = SystemTime::now()
2612 .duration_since(UNIX_EPOCH)
2613 .map(|duration| duration.as_nanos())
2614 .unwrap_or(0);
2615 for attempt in 0..100u32 {
2616 let name = format!(
2617 ".par2-create-{}-{stamp}-{index}-{attempt}.tmp",
2618 process_token()
2619 );
2620 let path = parent.join(name);
2621 match OpenOptions::new()
2624 .create_new(true)
2625 .read(true)
2626 .write(true)
2627 .open(&path)
2628 {
2629 Ok(file) => return Ok((path, file)),
2630 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
2631 Err(error) => return Err(Par2Error::Io(error)),
2632 }
2633 }
2634 Err(Par2Error::ResourceLimitExceeded {
2635 reason: "could not allocate a unique staged output path".to_string(),
2636 })
2637}
2638
2639fn reserve_backup_namespace(target: &Path, index: usize) -> Result<(PathBuf, PathBuf)> {
2640 reserve_private_namespace(target, index, "backup")
2641}
2642
2643fn reserve_quarantine_namespace(target: &Path, index: usize) -> Result<(PathBuf, PathBuf)> {
2644 reserve_private_namespace(target, index, "quarantine")
2645}
2646
2647fn reserve_private_namespace(
2648 target: &Path,
2649 index: usize,
2650 kind: &str,
2651) -> Result<(PathBuf, PathBuf)> {
2652 let parent = target.parent().unwrap_or_else(|| Path::new("."));
2653 let target_name = target
2654 .file_name()
2655 .ok_or_else(|| Par2Error::UnsafeCreationOutput {
2656 path: target.display().to_string(),
2657 reason: "output path has no filename".to_string(),
2658 })?;
2659 let stamp = SystemTime::now()
2660 .duration_since(UNIX_EPOCH)
2661 .map(|duration| duration.as_nanos())
2662 .unwrap_or(0);
2663 for attempt in 0..100u32 {
2664 let namespace = parent.join(format!(
2665 ".par2-create-{kind}-{}-{stamp}-{index}-{attempt}.tmp",
2666 process_token()
2667 ));
2668 match create_private_directory(&namespace) {
2669 Ok(()) => return Ok((namespace.clone(), namespace.join(target_name))),
2670 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
2671 Err(error) => return Err(Par2Error::Io(error)),
2672 }
2673 }
2674 Err(Par2Error::ResourceLimitExceeded {
2675 reason: "could not allocate a unique transaction path".to_string(),
2676 })
2677}
2678
2679fn create_private_directory(path: &Path) -> std::io::Result<()> {
2680 #[cfg(unix)]
2681 {
2682 use std::os::unix::fs::DirBuilderExt;
2683
2684 let mut builder = fs::DirBuilder::new();
2685 builder.mode(0o700);
2686 builder.create(path)
2687 }
2688 #[cfg(not(unix))]
2689 fs::create_dir(path)
2690}
2691
2692fn write_zeroes(
2693 file: &mut File,
2694 mut length: usize,
2695 cancellation: &CancellationToken,
2696) -> Result<()> {
2697 while length > 0 {
2698 check_cancel(cancellation)?;
2699 let take = length.min(ZERO_WRITE_BUFFER.len());
2700 file.write_all(&ZERO_WRITE_BUFFER[..take])
2701 .map_err(Par2Error::Io)?;
2702 length -= take;
2703 }
2704 Ok(())
2705}
2706
2707fn check_cancel(cancellation: &CancellationToken) -> Result<()> {
2708 if cancellation.is_cancelled() {
2709 Err(Par2Error::Cancelled)
2710 } else {
2711 Ok(())
2712 }
2713}
2714
2715fn pad_body(body: &mut Vec<u8>) -> Result<()> {
2716 let padding = (4 - body.len() % 4) % 4;
2717 body.try_reserve(padding)
2718 .map_err(|_| Par2Error::ResourceLimitExceeded {
2719 reason: "packet body allocation failed".to_string(),
2720 })?;
2721 body.resize(body.len() + padding, 0);
2722 Ok(())
2723}
2724
2725fn bit_length(value: u32) -> u32 {
2726 u32::BITS - value.leading_zeros()
2727}
2728
2729fn validation_error(path: &Path, reason: String) -> Par2Error {
2730 Par2Error::CreationValidation {
2731 path: path.display().to_string(),
2732 reason,
2733 }
2734}
2735
2736#[cfg(test)]
2737mod tests {
2738 use super::*;
2739
2740 #[test]
2741 fn zero_fill_honors_cancellation_before_writing() {
2742 let staged = tempfile::NamedTempFile::new().unwrap();
2743 let mut file = OpenOptions::new().write(true).open(staged.path()).unwrap();
2744 let cancellation = CancellationToken::new();
2745 cancellation.cancel();
2746
2747 let error =
2748 write_zeroes(&mut file, ZERO_WRITE_BUFFER.len() * 2, &cancellation).unwrap_err();
2749 assert!(matches!(error, Par2Error::Cancelled));
2750 assert_eq!(file.metadata().unwrap().len(), 0);
2751 }
2752
2753 #[test]
2754 fn cleanup_stage_files_drops_handles_before_removal() {
2755 let directory = tempfile::tempdir().unwrap();
2756 let stage_path = directory.path().join("stage.tmp");
2757 let file = OpenOptions::new()
2758 .create_new(true)
2759 .read(true)
2760 .write(true)
2761 .open(&stage_path)
2762 .unwrap();
2763 cleanup_stage_files(vec![VolumeState {
2764 stage_path: stage_path.clone(),
2765 target_path: directory.path().join("target.par2"),
2766 file,
2767 expected: Vec::new(),
2768 recovery_slots: Vec::new(),
2769 }]);
2770 assert!(!stage_path.exists());
2771 }
2772
2773 #[test]
2774 fn no_replace_publication_rejects_a_deterministic_target_race() {
2775 let directory = tempfile::tempdir().unwrap();
2776 let stage = directory.path().join("stage.tmp");
2777 let target = directory.path().join("set.par2");
2778 fs::write(&stage, b"staged").unwrap();
2779
2780 let error = publish_no_replace_with_hook(&stage, &target, || {
2781 fs::write(&target, b"foreign").unwrap();
2782 })
2783 .unwrap_err();
2784
2785 assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
2786 assert_eq!(fs::read(&stage).unwrap(), b"staged");
2787 assert_eq!(fs::read(&target).unwrap(), b"foreign");
2788 }
2789
2790 #[test]
2791 fn published_target_is_tracked_when_stage_unlink_fails() {
2792 let directory = tempfile::tempdir().unwrap();
2793 let stage = directory.path().join("stage.tmp");
2794 let target = directory.path().join("set.par2");
2795 fs::write(&stage, b"staged").unwrap();
2796
2797 let error = publish_no_replace_with_tracking_hooks(
2798 &stage,
2799 &target,
2800 || {},
2801 || {
2802 fs::remove_file(&stage).unwrap();
2803 fs::create_dir(&stage).unwrap();
2804 },
2805 )
2806 .unwrap_err();
2807
2808 assert!(matches!(error, PublishError::Installed { .. }));
2809 assert_eq!(fs::read(&target).unwrap(), b"staged");
2810 assert!(stage.is_dir());
2811 }
2812
2813 #[test]
2814 fn rollback_boundary_swap_preserves_a_foreign_target() {
2815 let directory = tempfile::tempdir().unwrap();
2816 let stage = directory.path().join("stage.tmp");
2817 let target = directory.path().join("set.par2");
2818 fs::write(&stage, b"staged").unwrap();
2819 let identity = publish_no_replace(&stage, &target)
2820 .unwrap()
2821 .expect("local test files have a strong identity");
2822
2823 let installed = InstalledTarget {
2824 path: target.clone(),
2825 identity: Some(identity),
2826 pin: Some(InodePin::open(&target, 1).unwrap()),
2827 };
2828
2829 let error = quarantine_owned_target_with_hook(&installed, || {
2833 fs::remove_file(&target).unwrap();
2834 fs::write(&target, b"foreign").unwrap();
2835 })
2836 .unwrap_err();
2837 assert!(matches!(error, Par2Error::CreationValidation { .. }));
2838 assert_eq!(fs::read(&target).unwrap(), b"foreign");
2839 assert!(fs::read_dir(directory.path()).unwrap().all(|entry| {
2840 !entry
2841 .unwrap()
2842 .file_name()
2843 .to_string_lossy()
2844 .starts_with(".par2-create-quarantine-")
2845 }));
2846 }
2847
2848 #[test]
2860 fn pinned_identity_refuses_a_recreated_file_even_when_the_inode_is_reused() {
2861 let directory = tempfile::tempdir().unwrap();
2862
2863 let control = directory.path().join("unpinned.par2");
2869 fs::write(&control, b"ours").unwrap();
2870 let control_before = file_identity_numbers(&control);
2871 fs::remove_file(&control).unwrap();
2872 fs::write(&control, b"foreign").unwrap();
2873 let control_after = file_identity_numbers(&control);
2874 eprintln!(
2875 "unpinned reuse: {} (before={control_before:?} after={control_after:?})",
2876 match reuse_verdict(control_before, control_after) {
2877 "YES" => "YES -- the pre-fix identity was forged",
2878 "no" => "no -- this filesystem did not recycle here",
2879 other => other,
2880 }
2881 );
2882
2883 let path = directory.path().join("owned.par2");
2884 fs::write(&path, b"ours").unwrap();
2885 let before = file_identity_numbers(&path);
2886 let pin = InodePin::open(&path, 1).unwrap();
2887 assert!(pin.still_at(&path).unwrap());
2888
2889 fs::remove_file(&path).unwrap();
2891 fs::write(&path, b"foreign").unwrap();
2892 let after = file_identity_numbers(&path);
2893
2894 eprintln!(
2897 "pinned reuse: {} (before={before:?} after={after:?})",
2898 reuse_verdict(before, after)
2899 );
2900 assert!(
2901 !pin.still_at(&path).unwrap(),
2902 "a recreated file must never satisfy the pin"
2903 );
2904 }
2905
2906 #[test]
2909 fn rollback_refuses_a_recreated_target_on_a_recycling_filesystem() {
2910 let directory = tempfile::tempdir().unwrap();
2911 let path = directory.path().join("set.par2");
2912 fs::write(&path, b"ours").unwrap();
2913 let identity = target_file_identity(&path).unwrap().unwrap();
2914 let before = file_identity_numbers(&path);
2915 let installed = InstalledTarget {
2916 path: path.clone(),
2917 identity: Some(identity),
2918 pin: Some(InodePin::open(&path, 1).unwrap()),
2919 };
2920
2921 fs::remove_file(&path).unwrap();
2922 fs::write(&path, b"foreign").unwrap();
2923 eprintln!(
2924 "inode reuse: {}",
2925 reuse_verdict(before, file_identity_numbers(&path))
2926 );
2927
2928 let error = quarantine_owned_target(&installed).unwrap_err();
2929 assert!(matches!(error, Par2Error::CreationValidation { .. }));
2930 assert_eq!(fs::read(&path).unwrap(), b"foreign");
2932 assert!(fs::read_dir(directory.path()).unwrap().all(|entry| {
2933 !entry
2934 .unwrap()
2935 .file_name()
2936 .to_string_lossy()
2937 .starts_with(".par2-create-quarantine-")
2938 }));
2939 }
2940
2941 #[cfg(windows)]
2956 #[test]
2957 fn a_pin_refuses_a_recreated_file_whose_creation_time_ntfs_replayed() {
2958 let directory = tempfile::tempdir().unwrap();
2959
2960 let control = directory.path().join("unpinned.par2");
2962 fs::write(&control, b"ours").unwrap();
2963 let birth_before = file_creation_ticks(&control);
2964 let numbers_before = file_identity_numbers(&control);
2965 fs::remove_file(&control).unwrap();
2966 fs::write(&control, b"foreign").unwrap();
2967 let birth_after = file_creation_ticks(&control);
2968 let numbers_after = file_identity_numbers(&control);
2969 eprintln!(
2970 "ntfs creation-time replay: {} (before={birth_before:?} after={birth_after:?})",
2971 if birth_before.is_some() && birth_before == birth_after {
2972 "YES -- tunneling forged the timestamp; a birth field alone would have been fooled"
2973 } else {
2974 "no -- tunneling is disabled or unsupported on this volume"
2975 }
2976 );
2977 eprintln!(
2978 "ntfs file-index repeat: {} (before={numbers_before:?} after={numbers_after:?})",
2979 reuse_verdict(numbers_before, numbers_after)
2980 );
2981
2982 let path = directory.path().join("owned.par2");
2985 fs::write(&path, b"ours").unwrap();
2986 let pin = InodePin::open(&path, 1).unwrap();
2987 assert!(pin.still_at(&path).unwrap());
2988 fs::remove_file(&path).unwrap();
2989 fs::write(&path, b"foreign").unwrap();
2990 assert!(
2991 !pin.still_at(&path).unwrap(),
2992 "a recreated file must never satisfy the pin"
2993 );
2994 assert_eq!(fs::read(&path).unwrap(), b"foreign");
2995 }
2996
2997 #[cfg(windows)]
2999 fn file_creation_ticks(path: &Path) -> Option<u64> {
3000 match target_file_identity(path) {
3001 Ok(Some(FileIdentity::Windows { birth, .. })) => birth,
3002 _ => None,
3003 }
3004 }
3005
3006 #[cfg(unix)]
3008 fn file_identity_numbers(path: &Path) -> Option<(u64, u64)> {
3009 use std::os::unix::fs::MetadataExt;
3010
3011 fs::symlink_metadata(path)
3012 .ok()
3013 .map(|metadata| (metadata.dev(), metadata.ino()))
3014 }
3015
3016 #[cfg(windows)]
3022 fn file_identity_numbers(path: &Path) -> Option<(u64, u64)> {
3023 match target_file_identity(path) {
3024 Ok(Some(FileIdentity::Windows { volume, index, .. })) => {
3025 Some((u64::from(volume), index))
3026 }
3027 _ => None,
3028 }
3029 }
3030
3031 #[cfg(not(any(unix, windows)))]
3032 fn file_identity_numbers(_path: &Path) -> Option<(u64, u64)> {
3033 None
3034 }
3035
3036 fn reuse_verdict(before: Option<(u64, u64)>, after: Option<(u64, u64)>) -> &'static str {
3039 match (before, after) {
3040 (Some(before), Some(after)) if before == after => "YES",
3041 (Some(_), Some(_)) => "no",
3042 _ => "unknown -- this target reports no file identity numbers",
3043 }
3044 }
3045
3046 #[test]
3047 fn overwrite_backup_boundary_restores_a_directory() {
3048 let directory = tempfile::tempdir().unwrap();
3049 let target = directory.path().join("set.par2");
3050 fs::write(&target, b"planned").unwrap();
3051 let staged = test_staged_outputs(&directory, &["set"]);
3052
3053 let error = staged
3054 .commit_with_transaction_hooks(
3055 true,
3056 &CancellationToken::new(),
3057 |target, _| {
3058 fs::remove_file(target).unwrap();
3059 fs::create_dir(target).unwrap();
3060 fs::write(target.join("foreign"), b"foreign directory material").unwrap();
3061 },
3062 |_, _| {},
3063 )
3064 .unwrap_err();
3065
3066 assert!(matches!(error, Par2Error::CreationValidation { .. }));
3067 assert!(target.is_dir());
3068 assert_eq!(
3069 fs::read(target.join("foreign")).unwrap(),
3070 b"foreign directory material"
3071 );
3072 assert!(fs::read_dir(directory.path()).unwrap().all(|entry| {
3073 !entry
3074 .unwrap()
3075 .file_name()
3076 .to_string_lossy()
3077 .starts_with(".par2-create-backup-")
3078 }));
3079 }
3080
3081 #[cfg(unix)]
3082 #[test]
3083 fn overwrite_backup_boundary_restores_a_symlink() {
3084 use std::os::unix::fs::symlink;
3085
3086 let directory = tempfile::tempdir().unwrap();
3087 let target = directory.path().join("set.par2");
3088 let referent = directory.path().join("foreign.bin");
3089 fs::write(&target, b"planned").unwrap();
3090 fs::write(&referent, b"foreign symlink material").unwrap();
3091 let staged = test_staged_outputs(&directory, &["set"]);
3092
3093 let error = staged
3094 .commit_with_transaction_hooks(
3095 true,
3096 &CancellationToken::new(),
3097 |target, _| {
3098 fs::remove_file(target).unwrap();
3099 symlink(&referent, target).unwrap();
3100 },
3101 |_, _| {},
3102 )
3103 .unwrap_err();
3104
3105 assert!(matches!(error, Par2Error::CreationValidation { .. }));
3106 assert!(
3107 fs::symlink_metadata(&target)
3108 .unwrap()
3109 .file_type()
3110 .is_symlink()
3111 );
3112 assert_eq!(fs::read_link(&target).unwrap(), referent);
3113 assert_eq!(fs::read(&target).unwrap(), b"foreign symlink material");
3114 assert!(fs::read_dir(directory.path()).unwrap().all(|entry| {
3115 !entry
3116 .unwrap()
3117 .file_name()
3118 .to_string_lossy()
3119 .starts_with(".par2-create-backup-")
3120 }));
3121 }
3122
3123 fn test_staged_outputs(directory: &tempfile::TempDir, names: &[&str]) -> StagedOutputs {
3124 let volumes: Vec<VolumeState> = names
3125 .iter()
3126 .map(|name| {
3127 let stage_path = directory.path().join(format!(".{name}.stage"));
3128 let target_path = directory.path().join(format!("{name}.par2"));
3129 let mut file = OpenOptions::new()
3130 .create_new(true)
3131 .read(true)
3132 .write(true)
3133 .open(&stage_path)
3134 .unwrap();
3135 file.write_all(format!("staged-{name}").as_bytes()).unwrap();
3136 VolumeState {
3137 stage_path,
3138 target_path,
3139 file,
3140 expected: Vec::new(),
3141 recovery_slots: Vec::new(),
3142 }
3143 })
3144 .collect();
3145 let planned_targets: Vec<TargetSnapshot> = volumes
3146 .iter()
3147 .map(|volume| capture_target_snapshot(&volume.target_path).unwrap())
3148 .collect();
3149 let planned_pins = volumes
3152 .iter()
3153 .zip(planned_targets.iter())
3154 .map(|(volume, snapshot)| match snapshot {
3155 TargetSnapshot::Absent => None,
3156 _ => Some(InodePin::open(&volume.target_path, volumes.len()).unwrap()),
3157 })
3158 .collect();
3159 StagedOutputs {
3160 critical_offsets: Vec::new(),
3161 volumes,
3162 locations: Vec::new(),
3163 planned_targets,
3164 planned_pins,
3165 committed: false,
3166 }
3167 }
3168
3169 #[test]
3170 fn overwrite_rejects_a_file_that_appears_after_absent_planning() {
3171 let directory = tempfile::tempdir().unwrap();
3172 let target = directory.path().join("set.par2");
3173 let staged = test_staged_outputs(&directory, &["set"]);
3174 fs::write(&target, b"foreign").unwrap();
3175
3176 let error = staged.commit(true, &CancellationToken::new()).unwrap_err();
3177
3178 assert!(matches!(error, Par2Error::CreationValidation { .. }));
3179 assert_eq!(fs::read(&target).unwrap(), b"foreign");
3180 }
3181
3182 #[test]
3183 fn overwrite_rejects_a_replacement_of_the_planned_target() {
3184 let directory = tempfile::tempdir().unwrap();
3185 let target = directory.path().join("set.par2");
3186 fs::write(&target, b"planned").unwrap();
3187 let staged = test_staged_outputs(&directory, &["set"]);
3188 fs::remove_file(&target).unwrap();
3189 fs::write(&target, b"foreign replacement").unwrap();
3190
3191 let error = staged.commit(true, &CancellationToken::new()).unwrap_err();
3192
3193 assert!(matches!(error, Par2Error::CreationValidation { .. }));
3194 assert_eq!(fs::read(&target).unwrap(), b"foreign replacement");
3195 }
3196
3197 #[test]
3198 fn overwrite_rejects_a_directory_that_appears_after_absent_planning() {
3199 let directory = tempfile::tempdir().unwrap();
3200 let target = directory.path().join("set.par2");
3201 let staged = test_staged_outputs(&directory, &["set"]);
3202 fs::create_dir(&target).unwrap();
3203 fs::write(target.join("foreign"), b"foreign directory material").unwrap();
3204
3205 let error = staged.commit(true, &CancellationToken::new()).unwrap_err();
3206
3207 assert!(matches!(error, Par2Error::CreationValidation { .. }));
3208 assert_eq!(
3209 fs::read(target.join("foreign")).unwrap(),
3210 b"foreign directory material"
3211 );
3212 }
3213
3214 #[test]
3215 fn no_overwrite_failure_after_an_earlier_output_keeps_foreign_output_and_quarantines_owned() {
3216 let directory = tempfile::tempdir().unwrap();
3217 let second = directory.path().join("second.par2");
3218 fs::write(&second, b"foreign").unwrap();
3219
3220 let staged = test_staged_outputs(&directory, &["first", "second"]);
3221 let error = staged.commit(false, &CancellationToken::new()).unwrap_err();
3222
3223 assert!(matches!(error, Par2Error::CreationOutputExists { .. }));
3224 assert!(!directory.path().join("first.par2").exists());
3225 assert_eq!(fs::read(&second).unwrap(), b"foreign");
3226 assert!(fs::read_dir(directory.path()).unwrap().any(|entry| {
3227 let path = entry.unwrap().path();
3228 path.file_name().is_some_and(|name| {
3229 name.to_string_lossy()
3230 .starts_with(".par2-create-quarantine-")
3231 }) && path.join("first.par2").is_file()
3232 }));
3233 }
3234
3235 #[test]
3236 fn overwrite_failure_after_an_earlier_output_restores_owned_and_preserves_foreign() {
3237 let directory = tempfile::tempdir().unwrap();
3238 let first = directory.path().join("first.par2");
3239 let second = directory.path().join("second.par2");
3240 fs::write(&first, b"old-first").unwrap();
3241 fs::write(&second, b"old-second").unwrap();
3242
3243 let staged = test_staged_outputs(&directory, &["first", "second"]);
3244 let error = staged
3245 .commit_with_publish_hook(true, &CancellationToken::new(), |_, target| {
3246 if target == second.as_path() {
3247 fs::write(target, b"foreign").unwrap();
3248 }
3249 })
3250 .unwrap_err();
3251
3252 assert!(matches!(error, Par2Error::CreationValidation { .. }));
3253 assert_eq!(fs::read(&first).unwrap(), b"old-first");
3254 assert_eq!(fs::read(&second).unwrap(), b"foreign");
3255 assert!(fs::read_dir(directory.path()).unwrap().any(|entry| {
3256 let path = entry.unwrap().path();
3257 path.file_name()
3258 .is_some_and(|name| name.to_string_lossy().starts_with(".par2-create-backup-"))
3259 && path.join("second.par2").is_file()
3260 }));
3261 }
3262
3263 #[test]
3264 fn cancellation_during_staged_validation_cleans_every_stage() {
3265 use std::sync::atomic::Ordering;
3266
3267 let directory = tempfile::tempdir().unwrap();
3268 let source = directory.path().join("source.bin");
3269 let output = directory.path().join("cancelled");
3270 fs::write(&source, b"staged validation cancellation").unwrap();
3271
3272 let cancellation = CancellationToken::new();
3273 let mut options = Par2CreatorOptions::with_output(
3274 output.clone(),
3275 Some(directory.path().to_path_buf()),
3276 vec![source],
3277 );
3278 options.recovery_amount = super::super::options::RecoveryAmount::Count(0);
3279 options.cancellation = cancellation;
3280 let creator = crate::create::Par2Creator::new(options);
3281 let plan = creator.plan().unwrap();
3282
3283 CANCEL_AFTER_VALIDATION_SCAN.store(true, Ordering::Relaxed);
3284 let error = creator.create(&plan).unwrap_err();
3285
3286 assert!(matches!(error, Par2Error::Cancelled));
3287 assert!(!output.with_extension("par2").exists());
3288 assert!(fs::read_dir(directory.path()).unwrap().all(|entry| {
3289 !entry
3290 .unwrap()
3291 .file_name()
3292 .to_string_lossy()
3293 .starts_with(".par2-create-")
3294 }));
3295 }
3296
3297 #[test]
3298 fn backup_namespace_is_atomically_reserved_and_cleanable() {
3299 let directory = tempfile::tempdir().unwrap();
3300 let target = directory.path().join("set.par2");
3301 fs::write(&target, b"old").unwrap();
3302 let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
3303 let (first, second) = std::thread::scope(|scope| {
3304 let first_barrier = std::sync::Arc::clone(&barrier);
3305 let first_target = target.clone();
3306 let first = scope.spawn(move || {
3307 first_barrier.wait();
3308 reserve_backup_namespace(&first_target, 0).unwrap()
3309 });
3310 let second_barrier = std::sync::Arc::clone(&barrier);
3311 let second_target = target.clone();
3312 let second = scope.spawn(move || {
3313 second_barrier.wait();
3314 reserve_backup_namespace(&second_target, 0).unwrap()
3315 });
3316 (first.join().unwrap(), second.join().unwrap())
3317 });
3318 let (namespace, backup) = first;
3319 let (second_namespace, second_backup) = second;
3320 assert!(namespace.is_dir());
3321 assert!(second_namespace.is_dir());
3322 assert_ne!(namespace, second_namespace);
3323 assert!(!backup.exists());
3324 assert!(!second_backup.exists());
3325 fs::rename(&target, &backup).unwrap();
3326 assert!(!target.exists());
3327 fs::rename(&backup, &target).unwrap();
3328 fs::remove_dir(&namespace).unwrap();
3329 fs::remove_dir(&second_namespace).unwrap();
3330 assert_eq!(fs::read(&target).unwrap(), b"old");
3331 }
3332}