1use std::collections::HashMap;
4#[cfg(unix)]
5use std::ffi::{CString, OsStr};
6#[cfg(not(windows))]
7use std::fs::OpenOptions;
8use std::fs::{self, File};
9use std::io::{self, Read, Seek, SeekFrom, Write};
10#[cfg(unix)]
11use std::os::fd::{AsRawFd, FromRawFd};
12#[cfg(unix)]
13use std::os::unix::ffi::OsStrExt;
14#[cfg(unix)]
15use std::os::unix::fs::OpenOptionsExt;
16use std::path::{Component, Path, PathBuf};
17
18use crate::par2_set::Par2FileSet;
19use crate::placement::PlacementPlan;
20use crate::types::FileId;
21use crate::verify::{FileAccess, FileRangeReader};
22
23fn repair_path_components(path: &Path) -> io::Result<Vec<&std::ffi::OsStr>> {
24 let mut components = Vec::new();
25 for component in path.components() {
26 match component {
27 Component::Normal(name) => components.push(name),
28 Component::CurDir => {}
29 Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
30 return Err(io::Error::new(
31 io::ErrorKind::InvalidInput,
32 format!(
33 "repair destination must remain relative to the working directory: {}",
34 path.display()
35 ),
36 ));
37 }
38 }
39 }
40 if components.is_empty() {
41 return Err(io::Error::new(
42 io::ErrorKind::InvalidInput,
43 "repair destination has no filename",
44 ));
45 }
46 Ok(components)
47}
48
49pub(crate) fn read_filled<R: Read + ?Sized>(reader: &mut R, dst: &mut [u8]) -> io::Result<usize> {
72 let mut filled = 0usize;
73 while filled < dst.len() {
74 match reader.read(&mut dst[filled..]) {
75 Ok(0) => break,
76 Ok(read) => filled += read,
77 Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
78 Err(error) => return Err(error),
79 }
80 }
81 Ok(filled)
82}
83
84#[cfg(unix)]
85fn c_component(component: &OsStr) -> io::Result<CString> {
86 CString::new(component.as_bytes()).map_err(|_| {
87 io::Error::new(
88 io::ErrorKind::InvalidInput,
89 "repair destination contains a NUL byte",
90 )
91 })
92}
93
94#[cfg(unix)]
95fn open_directory_at(parent: &File, component: &OsStr, create: bool) -> io::Result<File> {
96 let component = c_component(component)?;
97 let flags = libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW;
98 let open = || {
99 let fd = unsafe { libc::openat(parent.as_raw_fd(), component.as_ptr(), flags) };
102 if fd < 0 {
103 Err(io::Error::last_os_error())
104 } else {
105 Ok(unsafe { File::from_raw_fd(fd) })
107 }
108 };
109
110 match open() {
111 Ok(directory) => Ok(directory),
112 Err(error) if create && error.kind() == io::ErrorKind::NotFound => {
113 let result = unsafe {
116 libc::mkdirat(
117 parent.as_raw_fd(),
118 component.as_ptr(),
119 0o777 as libc::mode_t,
120 )
121 };
122 if result < 0 {
123 let error = io::Error::last_os_error();
124 if error.kind() != io::ErrorKind::AlreadyExists {
125 return Err(error);
126 }
127 }
128 open()
129 }
130 Err(error) => Err(error),
131 }
132}
133
134#[cfg(unix)]
135fn open_repair_output(base_dir: &Path, destination: &Path) -> io::Result<File> {
136 let relative = destination.strip_prefix(base_dir).map_err(|_| {
137 io::Error::new(
138 io::ErrorKind::InvalidInput,
139 "repair destination is outside the working directory",
140 )
141 })?;
142 let components = repair_path_components(relative)?;
143 let (filename, parent_components) = components.split_last().expect("checked non-empty");
144
145 let mut directory = OpenOptions::new()
149 .read(true)
150 .custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC)
151 .open(base_dir)?;
152 for component in parent_components {
153 directory = open_directory_at(&directory, component, true)?;
154 }
155
156 let filename = c_component(filename)?;
157 let flags = libc::O_WRONLY | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW;
158 let fd = unsafe {
161 libc::openat(
162 directory.as_raw_fd(),
163 filename.as_ptr(),
164 flags,
165 0o666 as libc::c_uint,
166 )
167 };
168 if fd < 0 {
169 Err(io::Error::last_os_error())
170 } else {
171 Ok(unsafe { File::from_raw_fd(fd) })
173 }
174}
175
176#[cfg(unix)]
177fn unix_repair_parent(
178 mut directory: File,
179 components: &[&OsStr],
180 create: bool,
181) -> io::Result<(File, CString)> {
182 let (filename, parent_components) = components.split_last().expect("checked non-empty");
183 for component in parent_components {
184 directory = open_directory_at(&directory, component, create)?;
185 }
186 Ok((directory, c_component(filename)?))
187}
188
189#[cfg(unix)]
190pub(crate) fn rename_within_base(base_dir: &Path, from: &Path, to: &Path) -> io::Result<()> {
191 let from_relative = from.strip_prefix(base_dir).map_err(|_| {
192 io::Error::new(
193 io::ErrorKind::InvalidInput,
194 "repair rename source is outside the working directory",
195 )
196 })?;
197 let to_relative = to.strip_prefix(base_dir).map_err(|_| {
198 io::Error::new(
199 io::ErrorKind::InvalidInput,
200 "repair rename destination is outside the working directory",
201 )
202 })?;
203 let from_components = repair_path_components(from_relative)?;
204 let to_components = repair_path_components(to_relative)?;
205 let root = OpenOptions::new()
206 .read(true)
207 .custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC)
208 .open(base_dir)?;
209 let (from_parent, from_name) = unix_repair_parent(root.try_clone()?, &from_components, false)?;
210 let (to_parent, to_name) = unix_repair_parent(root, &to_components, true)?;
211
212 let result = unsafe {
216 libc::renameat(
217 from_parent.as_raw_fd(),
218 from_name.as_ptr(),
219 to_parent.as_raw_fd(),
220 to_name.as_ptr(),
221 )
222 };
223 if result < 0 {
224 Err(io::Error::last_os_error())
225 } else {
226 Ok(())
227 }
228}
229
230#[cfg(unix)]
231pub(crate) fn remove_file_within_base(base_dir: &Path, path: &Path) -> io::Result<()> {
232 let relative = path.strip_prefix(base_dir).map_err(|_| {
233 io::Error::new(
234 io::ErrorKind::InvalidInput,
235 "repair removal target is outside the working directory",
236 )
237 })?;
238 let components = repair_path_components(relative)?;
239 let root = OpenOptions::new()
240 .read(true)
241 .custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC)
242 .open(base_dir)?;
243 let (parent, filename) = unix_repair_parent(root, &components, false)?;
244
245 let result = unsafe { libc::unlinkat(parent.as_raw_fd(), filename.as_ptr(), 0) };
249 if result < 0 {
250 Err(io::Error::last_os_error())
251 } else {
252 Ok(())
253 }
254}
255
256#[cfg(windows)]
257fn open_repair_output(base_dir: &Path, destination: &Path) -> io::Result<File> {
258 use cap_std::ambient_authority;
259 use cap_std::fs::{Dir, OpenOptions as CapOpenOptions};
260
261 let relative = destination.strip_prefix(base_dir).map_err(|_| {
262 io::Error::new(
263 io::ErrorKind::InvalidInput,
264 "repair destination is outside the working directory",
265 )
266 })?;
267 let components = repair_path_components(relative)?;
268 let (filename, parent_components) = components.split_last().expect("checked non-empty");
269 let mut directory = Dir::open_ambient_dir(base_dir, ambient_authority())?;
270
271 for component in parent_components {
272 match directory.symlink_metadata(component) {
273 Ok(metadata) if metadata.file_type().is_symlink() => {
274 return Err(io::Error::new(
275 io::ErrorKind::InvalidInput,
276 format!(
277 "repair destination traverses a link: {}",
278 destination.display()
279 ),
280 ));
281 }
282 Ok(metadata) if !metadata.is_dir() => {
283 return Err(io::Error::new(
284 io::ErrorKind::NotADirectory,
285 format!(
286 "repair destination parent is not a directory: {}",
287 destination.display()
288 ),
289 ));
290 }
291 Ok(_) => {}
292 Err(error) if error.kind() == io::ErrorKind::NotFound => {
293 directory.create_dir(component)?;
294 }
295 Err(error) => return Err(error),
296 }
297 directory = directory.open_dir(component)?;
300 }
301
302 match directory.symlink_metadata(filename) {
303 Ok(metadata) if metadata.file_type().is_symlink() => {
304 return Err(io::Error::new(
305 io::ErrorKind::InvalidInput,
306 format!("repair destination is a link: {}", destination.display()),
307 ));
308 }
309 Ok(_) => {}
310 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
311 Err(error) => return Err(error),
312 }
313
314 let mut options = CapOpenOptions::new();
315 options.write(true).create(true).truncate(false);
316 directory
317 .open_with(filename, &options)
318 .map(cap_std::fs::File::into_std)
319}
320
321#[cfg(windows)]
322pub(crate) fn rename_within_base(base_dir: &Path, from: &Path, to: &Path) -> io::Result<()> {
323 use cap_std::ambient_authority;
324 use cap_std::fs::Dir;
325
326 let from_relative = from.strip_prefix(base_dir).map_err(|_| {
327 io::Error::new(
328 io::ErrorKind::InvalidInput,
329 "repair rename source is outside the working directory",
330 )
331 })?;
332 let to_relative = to.strip_prefix(base_dir).map_err(|_| {
333 io::Error::new(
334 io::ErrorKind::InvalidInput,
335 "repair rename destination is outside the working directory",
336 )
337 })?;
338 repair_path_components(from_relative)?;
339 repair_path_components(to_relative)?;
340 let directory = Dir::open_ambient_dir(base_dir, ambient_authority())?;
341 if let Some(parent) = to_relative.parent() {
342 directory.create_dir_all(parent)?;
343 }
344 directory.rename(from_relative, &directory, to_relative)
345}
346
347#[cfg(windows)]
348pub(crate) fn remove_file_within_base(base_dir: &Path, path: &Path) -> io::Result<()> {
349 use cap_std::ambient_authority;
350 use cap_std::fs::Dir;
351
352 let relative = path.strip_prefix(base_dir).map_err(|_| {
353 io::Error::new(
354 io::ErrorKind::InvalidInput,
355 "repair removal target is outside the working directory",
356 )
357 })?;
358 repair_path_components(relative)?;
359 Dir::open_ambient_dir(base_dir, ambient_authority())?.remove_file(relative)
360}
361
362#[cfg(not(any(unix, windows)))]
363fn open_repair_output(base_dir: &Path, destination: &Path) -> io::Result<File> {
364 let relative = destination.strip_prefix(base_dir).map_err(|_| {
365 io::Error::new(
366 io::ErrorKind::InvalidInput,
367 "repair destination is outside the working directory",
368 )
369 })?;
370 let components = repair_path_components(relative)?;
371 let mut current = base_dir.to_path_buf();
372 for component in &components[..components.len() - 1] {
373 current.push(component);
374 match fs::symlink_metadata(¤t) {
375 Ok(metadata) if metadata.file_type().is_symlink() => {
376 return Err(io::Error::new(
377 io::ErrorKind::InvalidInput,
378 format!("repair destination traverses a link: {}", current.display()),
379 ));
380 }
381 Ok(metadata) if !metadata.is_dir() => {
382 return Err(io::Error::new(
383 io::ErrorKind::NotADirectory,
384 format!(
385 "repair destination parent is not a directory: {}",
386 current.display()
387 ),
388 ));
389 }
390 Ok(_) => {}
391 Err(error) if error.kind() == io::ErrorKind::NotFound => {
392 fs::create_dir(¤t)?;
393 let metadata = fs::symlink_metadata(¤t)?;
394 if metadata.file_type().is_symlink() || !metadata.is_dir() {
395 return Err(io::Error::new(
396 io::ErrorKind::InvalidInput,
397 format!("repair destination parent changed: {}", current.display()),
398 ));
399 }
400 }
401 Err(error) => return Err(error),
402 }
403 }
404
405 match fs::symlink_metadata(destination) {
406 Ok(metadata) if metadata.file_type().is_symlink() => Err(io::Error::new(
407 io::ErrorKind::InvalidInput,
408 format!("repair destination is a link: {}", destination.display()),
409 )),
410 Ok(_) | Err(_) => OpenOptions::new()
411 .write(true)
412 .create(true)
413 .truncate(false)
414 .open(destination),
415 }
416}
417
418#[cfg(not(any(unix, windows)))]
419pub(crate) fn rename_within_base(base_dir: &Path, from: &Path, to: &Path) -> io::Result<()> {
420 let from_relative = from.strip_prefix(base_dir).map_err(|_| {
421 io::Error::new(
422 io::ErrorKind::InvalidInput,
423 "repair rename source is outside the working directory",
424 )
425 })?;
426 let to_relative = to.strip_prefix(base_dir).map_err(|_| {
427 io::Error::new(
428 io::ErrorKind::InvalidInput,
429 "repair rename destination is outside the working directory",
430 )
431 })?;
432 repair_path_components(from_relative)?;
433 repair_path_components(to_relative)?;
434 if let Some(parent) = to.parent() {
435 fs::create_dir_all(parent)?;
436 }
437 fs::rename(from, to)
438}
439
440#[cfg(not(any(unix, windows)))]
441pub(crate) fn remove_file_within_base(base_dir: &Path, path: &Path) -> io::Result<()> {
442 let relative = path.strip_prefix(base_dir).map_err(|_| {
443 io::Error::new(
444 io::ErrorKind::InvalidInput,
445 "repair removal target is outside the working directory",
446 )
447 })?;
448 repair_path_components(relative)?;
449 fs::remove_file(path)
450}
451
452pub struct DiskFileAccess {
458 base_dir: PathBuf,
460 file_map: HashMap<FileId, String>,
462 write_files: HashMap<FileId, File>,
465}
466
467impl DiskFileAccess {
468 pub fn new(base_dir: PathBuf, par2_set: &Par2FileSet) -> Self {
472 let mut file_map = HashMap::new();
473 for (file_id, desc) in &par2_set.files {
474 file_map.insert(*file_id, desc.filename.clone());
475 }
476 Self {
477 base_dir,
478 file_map,
479 write_files: HashMap::new(),
480 }
481 }
482
483 fn path_for(&self, file_id: &FileId) -> Option<PathBuf> {
485 self.file_map
486 .get(file_id)
487 .map(|name| self.base_dir.join(name))
488 }
489}
490
491impl FileAccess for DiskFileAccess {
492 fn read_file_range(&self, file_id: &FileId, offset: u64, len: u64) -> io::Result<Vec<u8>> {
493 let path = self
494 .path_for(file_id)
495 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
496 let mut file = File::open(&path)?;
497 file.seek(SeekFrom::Start(offset))?;
498 let mut buf = vec![0u8; len as usize];
499 let n = read_filled(&mut file, &mut buf)?;
500 buf.truncate(n);
501 Ok(buf)
502 }
503
504 fn read_file_range_into(
505 &self,
506 file_id: &FileId,
507 offset: u64,
508 dst: &mut [u8],
509 ) -> io::Result<usize> {
510 let path = self
511 .path_for(file_id)
512 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
513 let mut file = File::open(&path)?;
514 file.seek(SeekFrom::Start(offset))?;
515 read_filled(&mut file, dst)
516 }
517
518 fn open_sequential_reader(&self, file_id: &FileId) -> io::Result<Option<Box<dyn Read>>> {
519 let path = self
520 .path_for(file_id)
521 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
522 Ok(Some(Box::new(crate::file_cache::CacheAdvisedReader::open(
523 &path,
524 )?)))
525 }
526
527 fn open_range_reader(&self, file_id: &FileId) -> io::Result<Option<Box<dyn FileRangeReader>>> {
528 let path = self
529 .path_for(file_id)
530 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
531 Ok(Some(Box::new(File::open(path)?)))
532 }
533
534 fn file_exists(&self, file_id: &FileId) -> bool {
535 self.path_for(file_id).map(|p| p.exists()).unwrap_or(false)
536 }
537
538 fn file_length(&self, file_id: &FileId) -> Option<u64> {
539 let path = self.path_for(file_id)?;
540 fs::metadata(&path).ok().map(|m| m.len())
541 }
542
543 fn read_file(&self, file_id: &FileId) -> io::Result<Vec<u8>> {
544 let path = self
545 .path_for(file_id)
546 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
547 crate::file_cache::read_to_vec(&path)
548 }
549
550 fn write_file_range(&mut self, file_id: &FileId, offset: u64, data: &[u8]) -> io::Result<()> {
551 let path = self
552 .path_for(file_id)
553 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
554 let file = if let Some(file) = self.write_files.get_mut(file_id) {
555 file
556 } else {
557 let file = open_repair_output(&self.base_dir, &path)?;
558 self.write_files.entry(*file_id).or_insert(file)
559 };
560 file.seek(SeekFrom::Start(offset))?;
561 file.write_all(data)?;
562 Ok(())
563 }
564}
565
566pub struct PlacementFileAccess {
571 base_dir: PathBuf,
572 file_map: HashMap<FileId, String>,
573 overrides: HashMap<FileId, String>,
574 write_files: HashMap<FileId, File>,
575}
576
577impl PlacementFileAccess {
578 pub fn new(
579 base_dir: PathBuf,
580 par2_set: &Par2FileSet,
581 overrides: HashMap<FileId, String>,
582 ) -> Self {
583 let mut file_map = HashMap::new();
584 for (file_id, desc) in &par2_set.files {
585 file_map.insert(*file_id, desc.filename.clone());
586 }
587 Self {
588 base_dir,
589 file_map,
590 overrides,
591 write_files: HashMap::new(),
592 }
593 }
594
595 pub fn from_plan(base_dir: PathBuf, par2_set: &Par2FileSet, plan: &PlacementPlan) -> Self {
596 let mut overrides = HashMap::new();
597
598 for (a, b) in &plan.swaps {
599 overrides.insert(a.file_id, a.current_name.clone());
600 overrides.insert(b.file_id, b.current_name.clone());
601 }
602 for entry in &plan.renames {
603 overrides.insert(entry.file_id, entry.current_name.clone());
604 }
605
606 Self::new(base_dir, par2_set, overrides)
607 }
608
609 fn path_for(&self, file_id: &FileId) -> Option<PathBuf> {
610 let name = self
611 .overrides
612 .get(file_id)
613 .or_else(|| self.file_map.get(file_id))?;
614 Some(self.base_dir.join(name))
615 }
616}
617
618impl FileAccess for PlacementFileAccess {
619 fn read_file_range(&self, file_id: &FileId, offset: u64, len: u64) -> io::Result<Vec<u8>> {
620 let path = self
621 .path_for(file_id)
622 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
623 let mut file = File::open(&path)?;
624 file.seek(SeekFrom::Start(offset))?;
625 let mut buf = vec![0u8; len as usize];
626 let n = read_filled(&mut file, &mut buf)?;
627 buf.truncate(n);
628 Ok(buf)
629 }
630
631 fn read_file_range_into(
632 &self,
633 file_id: &FileId,
634 offset: u64,
635 dst: &mut [u8],
636 ) -> io::Result<usize> {
637 let path = self
638 .path_for(file_id)
639 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
640 let mut file = File::open(&path)?;
641 file.seek(SeekFrom::Start(offset))?;
642 read_filled(&mut file, dst)
643 }
644
645 fn open_sequential_reader(&self, file_id: &FileId) -> io::Result<Option<Box<dyn Read>>> {
646 let path = self
647 .path_for(file_id)
648 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
649 Ok(Some(Box::new(crate::file_cache::CacheAdvisedReader::open(
650 &path,
651 )?)))
652 }
653
654 fn open_range_reader(&self, file_id: &FileId) -> io::Result<Option<Box<dyn FileRangeReader>>> {
655 let path = self
656 .path_for(file_id)
657 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
658 Ok(Some(Box::new(File::open(path)?)))
659 }
660
661 fn file_exists(&self, file_id: &FileId) -> bool {
662 self.path_for(file_id).map(|p| p.exists()).unwrap_or(false)
663 }
664
665 fn file_length(&self, file_id: &FileId) -> Option<u64> {
666 let path = self.path_for(file_id)?;
667 fs::metadata(&path).ok().map(|m| m.len())
668 }
669
670 fn read_file(&self, file_id: &FileId) -> io::Result<Vec<u8>> {
671 let path = self
672 .path_for(file_id)
673 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
674 crate::file_cache::read_to_vec(&path)
675 }
676
677 fn write_file_range(&mut self, file_id: &FileId, offset: u64, data: &[u8]) -> io::Result<()> {
678 let path = self
679 .path_for(file_id)
680 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "unknown file ID"))?;
681 let file = if let Some(file) = self.write_files.get_mut(file_id) {
682 file
683 } else {
684 let file = open_repair_output(&self.base_dir, &path)?;
685 self.write_files.entry(*file_id).or_insert(file)
686 };
687 file.seek(SeekFrom::Start(offset))?;
688 file.write_all(data)?;
689 Ok(())
690 }
691}
692
693pub struct MultiDirectoryFileAccess {
699 primary: DiskFileAccess,
700 search_dirs: Vec<DiskFileAccess>,
701}
702
703impl MultiDirectoryFileAccess {
704 pub fn new(primary_dir: PathBuf, search_dirs: Vec<PathBuf>, par2_set: &Par2FileSet) -> Self {
710 let primary = DiskFileAccess::new(primary_dir, par2_set);
711 let search = search_dirs
712 .into_iter()
713 .map(|dir| DiskFileAccess::new(dir, par2_set))
714 .collect();
715 Self {
716 primary,
717 search_dirs: search,
718 }
719 }
720
721 fn find_reader(&self, file_id: &FileId) -> Option<&DiskFileAccess> {
723 if self.primary.file_exists(file_id) {
724 return Some(&self.primary);
725 }
726 self.search_dirs.iter().find(|d| d.file_exists(file_id))
727 }
728}
729
730impl FileAccess for MultiDirectoryFileAccess {
731 fn read_file_range(&self, file_id: &FileId, offset: u64, len: u64) -> io::Result<Vec<u8>> {
732 match self.find_reader(file_id) {
733 Some(accessor) => accessor.read_file_range(file_id, offset, len),
734 None => Err(io::Error::new(
735 io::ErrorKind::NotFound,
736 "file not found in any directory",
737 )),
738 }
739 }
740
741 fn read_file_range_into(
742 &self,
743 file_id: &FileId,
744 offset: u64,
745 dst: &mut [u8],
746 ) -> io::Result<usize> {
747 match self.find_reader(file_id) {
748 Some(accessor) => accessor.read_file_range_into(file_id, offset, dst),
749 None => Err(io::Error::new(
750 io::ErrorKind::NotFound,
751 "file not found in any directory",
752 )),
753 }
754 }
755
756 fn open_sequential_reader(&self, file_id: &FileId) -> io::Result<Option<Box<dyn Read>>> {
757 match self.find_reader(file_id) {
758 Some(accessor) => accessor.open_sequential_reader(file_id),
759 None => Err(io::Error::new(
760 io::ErrorKind::NotFound,
761 "file not found in any directory",
762 )),
763 }
764 }
765
766 fn open_range_reader(&self, file_id: &FileId) -> io::Result<Option<Box<dyn FileRangeReader>>> {
767 match self.find_reader(file_id) {
768 Some(accessor) => accessor.open_range_reader(file_id),
769 None => Err(io::Error::new(
770 io::ErrorKind::NotFound,
771 "file not found in any directory",
772 )),
773 }
774 }
775
776 fn file_exists(&self, file_id: &FileId) -> bool {
777 self.find_reader(file_id).is_some()
778 }
779
780 fn file_length(&self, file_id: &FileId) -> Option<u64> {
781 self.find_reader(file_id)?.file_length(file_id)
782 }
783
784 fn read_file(&self, file_id: &FileId) -> io::Result<Vec<u8>> {
785 match self.find_reader(file_id) {
786 Some(accessor) => accessor.read_file(file_id),
787 None => Err(io::Error::new(
788 io::ErrorKind::NotFound,
789 "file not found in any directory",
790 )),
791 }
792 }
793
794 fn write_file_range(&mut self, file_id: &FileId, offset: u64, data: &[u8]) -> io::Result<()> {
795 self.primary.write_file_range(file_id, offset, data)
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use crate::checksum;
803 use crate::checksum::SliceChecksumState;
804 use crate::packet::header;
805 use crate::par2_set::Par2FileSet;
806 use crate::placement::scan_placement;
807 use crate::types::SliceChecksum;
808 use crate::verify::{FileStatus, verify_all};
809 use md5::{Digest, Md5};
810 use tempfile::TempDir;
811
812 fn make_full_packet(packet_type: &[u8; 16], body: &[u8], recovery_set_id: [u8; 16]) -> Vec<u8> {
814 let length = (header::HEADER_SIZE + body.len()) as u64;
815 let mut hash_input = Vec::new();
816 hash_input.extend_from_slice(&recovery_set_id);
817 hash_input.extend_from_slice(packet_type);
818 hash_input.extend_from_slice(body);
819 let packet_hash: [u8; 16] = Md5::digest(&hash_input).into();
820
821 let mut data = Vec::new();
822 data.extend_from_slice(header::MAGIC);
823 data.extend_from_slice(&length.to_le_bytes());
824 data.extend_from_slice(&packet_hash);
825 data.extend_from_slice(&recovery_set_id);
826 data.extend_from_slice(packet_type);
827 data.extend_from_slice(body);
828 data
829 }
830
831 fn setup_par2_set(file_data: &[u8], slice_size: u64, filename: &str) -> (Par2FileSet, FileId) {
833 let file_length = file_data.len() as u64;
834 let hash_full = checksum::md5(file_data);
835 let hash_16k_data = &file_data[..file_data.len().min(16384)];
836 let hash_16k = checksum::md5(hash_16k_data);
837
838 let mut id_input = Vec::new();
839 id_input.extend_from_slice(&hash_16k);
840 id_input.extend_from_slice(&file_length.to_le_bytes());
841 id_input.extend_from_slice(filename.as_bytes());
842 let file_id_bytes: [u8; 16] = Md5::digest(&id_input).into();
843 let file_id = FileId::from_bytes(file_id_bytes);
844
845 let num_slices = if file_length == 0 {
846 0
847 } else {
848 file_length.div_ceil(slice_size) as usize
849 };
850
851 let mut checksums = Vec::new();
852 for i in 0..num_slices {
853 let offset = i as u64 * slice_size;
854 let end = ((offset + slice_size) as usize).min(file_data.len());
855 let slice_data = &file_data[offset as usize..end];
856 let mut state = SliceChecksumState::new();
857 state.update(slice_data);
858 let pad_to = if (slice_data.len() as u64) < slice_size {
859 Some(slice_size)
860 } else {
861 None
862 };
863 let (crc, md5) = state.finalize(pad_to);
864 checksums.push(SliceChecksum { crc32: crc, md5 });
865 }
866
867 let mut main_body = Vec::new();
868 main_body.extend_from_slice(&slice_size.to_le_bytes());
869 main_body.extend_from_slice(&1u32.to_le_bytes());
870 main_body.extend_from_slice(&file_id_bytes);
871 let rsid: [u8; 16] = Md5::digest(&main_body).into();
872
873 let mut fd_body = Vec::new();
874 fd_body.extend_from_slice(&file_id_bytes);
875 fd_body.extend_from_slice(&hash_full);
876 fd_body.extend_from_slice(&hash_16k);
877 fd_body.extend_from_slice(&file_length.to_le_bytes());
878 fd_body.extend_from_slice(filename.as_bytes());
879 while fd_body.len() % 4 != 0 {
880 fd_body.push(0);
881 }
882
883 let mut ifsc_body = Vec::new();
884 ifsc_body.extend_from_slice(&file_id_bytes);
885 for cs in &checksums {
886 ifsc_body.extend_from_slice(&cs.md5);
887 ifsc_body.extend_from_slice(&cs.crc32.to_le_bytes());
888 }
889
890 let mut stream = Vec::new();
891 stream.extend_from_slice(&make_full_packet(header::TYPE_MAIN, &main_body, rsid));
892 stream.extend_from_slice(&make_full_packet(header::TYPE_FILE_DESC, &fd_body, rsid));
893 stream.extend_from_slice(&make_full_packet(header::TYPE_IFSC, &ifsc_body, rsid));
894
895 let set = Par2FileSet::from_files(&[&stream]).unwrap();
896 (set, file_id)
897 }
898
899 fn setup_par2_set_multi(
900 files: &[(&[u8], &str)],
901 slice_size: u64,
902 ) -> (Par2FileSet, Vec<FileId>) {
903 let mut file_ids = Vec::new();
904 let mut fd_bodies = Vec::new();
905 let mut ifsc_bodies = Vec::new();
906
907 for &(file_data, filename) in files {
908 let file_length = file_data.len() as u64;
909 let hash_full = checksum::md5(file_data);
910 let hash_16k = checksum::md5(&file_data[..file_data.len().min(16384)]);
911
912 let mut id_input = Vec::new();
913 id_input.extend_from_slice(&hash_16k);
914 id_input.extend_from_slice(&file_length.to_le_bytes());
915 id_input.extend_from_slice(filename.as_bytes());
916 let file_id_bytes: [u8; 16] = Md5::digest(&id_input).into();
917 file_ids.push(FileId::from_bytes(file_id_bytes));
918
919 let num_slices = if file_length == 0 {
920 0
921 } else {
922 file_length.div_ceil(slice_size) as usize
923 };
924
925 let mut checksums = Vec::new();
926 for i in 0..num_slices {
927 let offset = i as u64 * slice_size;
928 let end = ((offset + slice_size) as usize).min(file_data.len());
929 let slice_data = &file_data[offset as usize..end];
930 let mut state = SliceChecksumState::new();
931 state.update(slice_data);
932 let pad_to = if (slice_data.len() as u64) < slice_size {
933 Some(slice_size)
934 } else {
935 None
936 };
937 let (crc, md5) = state.finalize(pad_to);
938 checksums.push(SliceChecksum { crc32: crc, md5 });
939 }
940
941 let mut fd_body = Vec::new();
942 fd_body.extend_from_slice(&file_id_bytes);
943 fd_body.extend_from_slice(&hash_full);
944 fd_body.extend_from_slice(&hash_16k);
945 fd_body.extend_from_slice(&file_length.to_le_bytes());
946 fd_body.extend_from_slice(filename.as_bytes());
947 while fd_body.len() % 4 != 0 {
948 fd_body.push(0);
949 }
950 fd_bodies.push(fd_body);
951
952 let mut ifsc_body = Vec::new();
953 ifsc_body.extend_from_slice(&file_id_bytes);
954 for cs in &checksums {
955 ifsc_body.extend_from_slice(&cs.md5);
956 ifsc_body.extend_from_slice(&cs.crc32.to_le_bytes());
957 }
958 ifsc_bodies.push(ifsc_body);
959 }
960
961 let mut main_body = Vec::new();
962 main_body.extend_from_slice(&slice_size.to_le_bytes());
963 main_body.extend_from_slice(&(file_ids.len() as u32).to_le_bytes());
964 for file_id in &file_ids {
965 main_body.extend_from_slice(file_id.as_bytes());
966 }
967 let rsid: [u8; 16] = Md5::digest(&main_body).into();
968
969 let mut stream = Vec::new();
970 stream.extend_from_slice(&make_full_packet(header::TYPE_MAIN, &main_body, rsid));
971 for fd_body in &fd_bodies {
972 stream.extend_from_slice(&make_full_packet(header::TYPE_FILE_DESC, fd_body, rsid));
973 }
974 for ifsc_body in &ifsc_bodies {
975 stream.extend_from_slice(&make_full_packet(header::TYPE_IFSC, ifsc_body, rsid));
976 }
977
978 let set = Par2FileSet::from_files(&[&stream]).unwrap();
979 (set, file_ids)
980 }
981
982 #[test]
983 fn disk_access_read_write_exists_length() {
984 let dir = TempDir::new().unwrap();
985 let file_data = b"Hello, PAR2 world!";
986 let filename = "test.dat";
987
988 std::fs::write(dir.path().join(filename), file_data).unwrap();
990
991 let (par2_set, file_id) = setup_par2_set(file_data, 1024, filename);
992 let mut access = DiskFileAccess::new(dir.path().to_path_buf(), &par2_set);
993
994 assert!(access.file_exists(&file_id));
996
997 assert_eq!(access.file_length(&file_id), Some(file_data.len() as u64));
999
1000 let read_all = access.read_file(&file_id).unwrap();
1002 assert_eq!(read_all, file_data);
1003
1004 let range = access.read_file_range(&file_id, 7, 4).unwrap();
1006 assert_eq!(&range, b"PAR2");
1007
1008 access.write_file_range(&file_id, 7, b"par2").unwrap();
1010 access.write_file_range(&file_id, 11, b"!").unwrap();
1011 assert_eq!(access.write_files.len(), 1);
1012 let after_write = access.read_file_range(&file_id, 7, 4).unwrap();
1013 assert_eq!(&after_write, b"par2");
1014 }
1015
1016 #[test]
1017 fn disk_access_missing_file() {
1018 let dir = TempDir::new().unwrap();
1019 let file_data = b"data";
1020 let filename = "missing.dat";
1021
1022 let (par2_set, file_id) = setup_par2_set(file_data, 1024, filename);
1023 let access = DiskFileAccess::new(dir.path().to_path_buf(), &par2_set);
1024
1025 assert!(!access.file_exists(&file_id));
1026 assert_eq!(access.file_length(&file_id), None);
1027 assert!(access.read_file(&file_id).is_err());
1028 }
1029
1030 #[test]
1031 fn disk_access_unknown_file_id() {
1032 let dir = TempDir::new().unwrap();
1033 let file_data = b"data";
1034 let filename = "test.dat";
1035
1036 let (par2_set, _) = setup_par2_set(file_data, 1024, filename);
1037 let access = DiskFileAccess::new(dir.path().to_path_buf(), &par2_set);
1038
1039 let unknown_id = FileId::from_bytes([0xFF; 16]);
1040 assert!(!access.file_exists(&unknown_id));
1041 assert_eq!(access.file_length(&unknown_id), None);
1042 assert!(access.read_file(&unknown_id).is_err());
1043 }
1044
1045 #[test]
1046 fn disk_access_create_on_write() {
1047 let dir = TempDir::new().unwrap();
1048 let file_data = b"original";
1049 let filename = "newfile.dat";
1050
1051 let (par2_set, file_id) = setup_par2_set(file_data, 1024, filename);
1052 let mut access = DiskFileAccess::new(dir.path().to_path_buf(), &par2_set);
1053
1054 assert!(!access.file_exists(&file_id));
1056
1057 access.write_file_range(&file_id, 0, b"created").unwrap();
1059 assert!(access.file_exists(&file_id));
1060
1061 let content = access.read_file(&file_id).unwrap();
1062 assert_eq!(&content, b"created");
1063 }
1064
1065 #[cfg(unix)]
1066 #[test]
1067 fn disk_access_rejects_symlink_destination() {
1068 let dir = TempDir::new().unwrap();
1069 let outside = TempDir::new().unwrap();
1070 let outside_path = outside.path().join("outside.dat");
1071 std::fs::write(&outside_path, b"outside stays unchanged").unwrap();
1072 std::os::unix::fs::symlink(&outside_path, dir.path().join("victim.dat")).unwrap();
1073
1074 let (par2_set, file_id) = setup_par2_set(b"repaired data", 1024, "victim.dat");
1075 let mut access = DiskFileAccess::new(dir.path().to_path_buf(), &par2_set);
1076
1077 assert!(
1078 access
1079 .write_file_range(&file_id, 0, b"repaired data")
1080 .is_err()
1081 );
1082 assert_eq!(
1083 std::fs::read(&outside_path).unwrap(),
1084 b"outside stays unchanged"
1085 );
1086 assert!(
1087 std::fs::symlink_metadata(dir.path().join("victim.dat"))
1088 .unwrap()
1089 .file_type()
1090 .is_symlink()
1091 );
1092 }
1093
1094 #[cfg(unix)]
1095 #[test]
1096 fn disk_access_rejects_symlink_parent() {
1097 let dir = TempDir::new().unwrap();
1098 let outside = TempDir::new().unwrap();
1099 let outside_path = outside.path().join("victim.dat");
1100 std::fs::write(&outside_path, b"outside stays unchanged").unwrap();
1101 std::os::unix::fs::symlink(outside.path(), dir.path().join("nested")).unwrap();
1102
1103 let (par2_set, file_id) = setup_par2_set(b"repaired data", 1024, "nested/victim.dat");
1104 let mut access = DiskFileAccess::new(dir.path().to_path_buf(), &par2_set);
1105
1106 assert!(
1107 access
1108 .write_file_range(&file_id, 0, b"repaired data")
1109 .is_err()
1110 );
1111 assert_eq!(
1112 std::fs::read(&outside_path).unwrap(),
1113 b"outside stays unchanged"
1114 );
1115 assert!(
1116 std::fs::symlink_metadata(dir.path().join("nested"))
1117 .unwrap()
1118 .file_type()
1119 .is_symlink()
1120 );
1121 }
1122
1123 #[cfg(unix)]
1124 #[test]
1125 fn rename_within_base_rejects_symlink_parent() {
1126 let dir = TempDir::new().unwrap();
1127 let outside = TempDir::new().unwrap();
1128 let source = dir.path().join("source.dat");
1129 let outside_target = outside.path().join("victim.dat");
1130 std::fs::write(&source, b"repaired data").unwrap();
1131 std::fs::write(&outside_target, b"outside stays unchanged").unwrap();
1132 std::os::unix::fs::symlink(outside.path(), dir.path().join("nested")).unwrap();
1133
1134 assert!(
1135 rename_within_base(dir.path(), &source, &dir.path().join("nested/victim.dat")).is_err()
1136 );
1137 assert_eq!(std::fs::read(&source).unwrap(), b"repaired data");
1138 assert_eq!(
1139 std::fs::read(&outside_target).unwrap(),
1140 b"outside stays unchanged"
1141 );
1142 }
1143
1144 #[cfg(unix)]
1145 #[test]
1146 fn disk_access_allows_caller_selected_symlink_base() {
1147 let parent = TempDir::new().unwrap();
1148 let base = TempDir::new().unwrap();
1149 let base_link = parent.path().join("base-link");
1150 std::os::unix::fs::symlink(base.path(), &base_link).unwrap();
1151
1152 let (par2_set, file_id) = setup_par2_set(b"repaired data", 1024, "created.dat");
1153 let mut access = DiskFileAccess::new(base_link, &par2_set);
1154
1155 access
1156 .write_file_range(&file_id, 0, b"repaired data")
1157 .unwrap();
1158 assert_eq!(
1159 std::fs::read(base.path().join("created.dat")).unwrap(),
1160 b"repaired data"
1161 );
1162 }
1163
1164 #[test]
1165 fn multi_dir_finds_file_in_secondary() {
1166 let primary = TempDir::new().unwrap();
1167 let secondary = TempDir::new().unwrap();
1168 let file_data = b"found in secondary";
1169 let filename = "target.dat";
1170
1171 let (par2_set, file_id) = setup_par2_set(file_data, 1024, filename);
1172
1173 std::fs::write(secondary.path().join(filename), file_data).unwrap();
1175
1176 let access = MultiDirectoryFileAccess::new(
1177 primary.path().to_path_buf(),
1178 vec![secondary.path().to_path_buf()],
1179 &par2_set,
1180 );
1181
1182 assert!(access.file_exists(&file_id));
1183 assert_eq!(access.read_file(&file_id).unwrap(), file_data);
1184 }
1185
1186 #[test]
1187 fn multi_dir_primary_wins() {
1188 let primary = TempDir::new().unwrap();
1189 let secondary = TempDir::new().unwrap();
1190 let filename = "target.dat";
1191
1192 let (par2_set, file_id) = setup_par2_set(b"primary", 1024, filename);
1193
1194 std::fs::write(primary.path().join(filename), b"primary").unwrap();
1195 std::fs::write(secondary.path().join(filename), b"secondary").unwrap();
1196
1197 let access = MultiDirectoryFileAccess::new(
1198 primary.path().to_path_buf(),
1199 vec![secondary.path().to_path_buf()],
1200 &par2_set,
1201 );
1202
1203 let content = access.read_file(&file_id).unwrap();
1204 assert_eq!(&content, b"primary");
1205 }
1206
1207 #[test]
1208 fn multi_dir_write_goes_to_primary() {
1209 let primary = TempDir::new().unwrap();
1210 let secondary = TempDir::new().unwrap();
1211 let filename = "target.dat";
1212
1213 let (par2_set, file_id) = setup_par2_set(b"data", 1024, filename);
1214
1215 let mut access = MultiDirectoryFileAccess::new(
1216 primary.path().to_path_buf(),
1217 vec![secondary.path().to_path_buf()],
1218 &par2_set,
1219 );
1220
1221 access.write_file_range(&file_id, 0, b"written").unwrap();
1222 assert!(primary.path().join(filename).exists());
1223 assert!(!secondary.path().join(filename).exists());
1224 }
1225
1226 #[test]
1227 fn multi_dir_not_found_anywhere() {
1228 let primary = TempDir::new().unwrap();
1229 let secondary = TempDir::new().unwrap();
1230
1231 let (par2_set, file_id) = setup_par2_set(b"data", 1024, "missing.dat");
1232
1233 let access = MultiDirectoryFileAccess::new(
1234 primary.path().to_path_buf(),
1235 vec![secondary.path().to_path_buf()],
1236 &par2_set,
1237 );
1238
1239 assert!(!access.file_exists(&file_id));
1240 assert!(access.read_file(&file_id).is_err());
1241 }
1242
1243 struct CappedReader<'a> {
1247 data: &'a [u8],
1248 cap: usize,
1249 }
1250
1251 impl Read for CappedReader<'_> {
1252 fn read(&mut self, dst: &mut [u8]) -> io::Result<usize> {
1253 let take = dst.len().min(self.cap).min(self.data.len());
1254 dst[..take].copy_from_slice(&self.data[..take]);
1255 self.data = &self.data[take..];
1256 Ok(take)
1257 }
1258 }
1259
1260 #[test]
1261 fn read_filled_fills_across_short_reads() {
1262 let data: Vec<u8> = (0..200_000u32).map(|i| i as u8).collect();
1263 let mut reader = CappedReader {
1264 data: &data,
1265 cap: 65_536,
1266 };
1267 let mut dst = vec![0u8; data.len()];
1268
1269 let filled = read_filled(&mut reader, &mut dst).unwrap();
1270
1271 assert_eq!(filled, data.len(), "a capped reader must still fill dst");
1272 assert_eq!(dst, data);
1273 }
1274
1275 #[test]
1276 fn read_filled_stops_at_end_of_input() {
1277 let data = vec![7u8; 100];
1278 let mut reader = CappedReader {
1279 data: &data,
1280 cap: 8,
1281 };
1282 let mut dst = vec![0u8; 512];
1283
1284 let filled = read_filled(&mut reader, &mut dst).unwrap();
1285
1286 assert_eq!(filled, data.len());
1287 assert!(dst[data.len()..].iter().all(|&byte| byte == 0));
1288 }
1289
1290 #[test]
1297 fn disk_access_read_file_range_fills_beyond_one_host_read() {
1298 let dir = TempDir::new().unwrap();
1299 let data: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
1300 let (par2_set, file_id) = setup_par2_set(&data, 65_536, "big.dat");
1301 std::fs::write(dir.path().join("big.dat"), &data).unwrap();
1302
1303 let access = DiskFileAccess::new(dir.path().to_path_buf(), &par2_set);
1304
1305 let mut dst = vec![0u8; data.len()];
1306 let read = access.read_file_range_into(&file_id, 0, &mut dst).unwrap();
1307 assert_eq!(read, data.len());
1308 assert_eq!(dst, data);
1309
1310 let owned = access
1311 .read_file_range(&file_id, 0, data.len() as u64)
1312 .unwrap();
1313 assert_eq!(owned, data);
1314 }
1315
1316 #[test]
1317 fn placement_access_verifies_swapped_valid_names() {
1318 let dir = TempDir::new().unwrap();
1319 let data_a = b"placement-aware file A data";
1320 let data_b = b"placement-aware file B data";
1321
1322 let (par2_set, _ids) =
1323 setup_par2_set_multi(&[(data_a, "file_a.rar"), (data_b, "file_b.rar")], 1024);
1324
1325 std::fs::write(dir.path().join("file_a.rar"), data_b).unwrap();
1326 std::fs::write(dir.path().join("file_b.rar"), data_a).unwrap();
1327
1328 let plan = scan_placement(dir.path(), &par2_set).unwrap();
1329 let access = PlacementFileAccess::from_plan(dir.path().to_path_buf(), &par2_set, &plan);
1330 let result = verify_all(&par2_set, &access);
1331
1332 assert_eq!(result.total_missing_blocks, 0);
1333 assert!(
1334 result
1335 .files
1336 .iter()
1337 .all(|file| matches!(file.status, FileStatus::Complete))
1338 );
1339 }
1340}