1use cap_primitives::fs::FileType;
12use cap_std::fs::{Dir, File, Metadata};
13use cap_tempfile::cap_std;
14use cap_tempfile::cap_std::fs::DirEntry;
15#[cfg(any(target_os = "android", target_os = "linux"))]
16use rustix::path::Arg;
17use std::cmp::Ordering;
18use std::ffi::OsStr;
19use std::io::{self, Write};
20use std::io::{Read, Result};
21use std::ops::Deref;
22#[cfg(unix)]
23use std::os::fd::OwnedFd;
24use std::path::{Path, PathBuf};
25
26#[cfg(feature = "fs_utf8")]
27use cap_std::fs_utf8;
28#[cfg(feature = "fs_utf8")]
29use fs_utf8::camino::Utf8Path;
30
31#[non_exhaustive]
33#[derive(Debug)]
34pub struct WalkComponent<'p, 'd> {
35 pub path: &'p Path,
44 pub dir: &'d Dir,
46 pub filename: &'p OsStr,
49 pub file_type: FileType,
51 pub entry: &'p DirEntry,
53}
54
55#[non_exhaustive]
57#[derive(Default)]
58pub struct WalkConfiguration<'p> {
59 noxdev: bool,
61
62 path_base: Option<&'p Path>,
63
64 #[allow(clippy::type_complexity)]
66 sorter: Option<Box<dyn Fn(&DirEntry, &DirEntry) -> Ordering + 'static>>,
67}
68
69pub type WalkResult<E> = std::result::Result<std::ops::ControlFlow<()>, E>;
71
72impl std::fmt::Debug for WalkConfiguration<'_> {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("WalkConfiguration")
75 .field("noxdev", &self.noxdev)
76 .field("sorter", &self.sorter.as_ref().map(|_| true))
77 .finish()
78 }
79}
80
81impl<'p> WalkConfiguration<'p> {
82 pub fn noxdev(mut self) -> Self {
84 self.noxdev = true;
85 self
86 }
87
88 pub fn sort_by<F>(mut self, cmp: F) -> Self
90 where
91 F: Fn(&DirEntry, &DirEntry) -> Ordering + 'static,
92 {
93 self.sorter = Some(Box::new(cmp));
94 self
95 }
96
97 pub fn sort_by_file_name(self) -> Self {
99 self.sort_by(|a, b| a.file_name().cmp(&b.file_name()))
100 }
101
102 pub fn path_base(mut self, base: &'p Path) -> Self {
107 self.path_base = Some(base);
108 self
109 }
110}
111
112pub trait CapStdExtDirExt {
116 fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>>;
118
119 fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>>;
121
122 #[cfg(any(target_os = "android", target_os = "linux"))]
125 fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir>;
126
127 #[cfg(any(target_os = "android", target_os = "linux"))]
129 fn open_dir_noxdev(&self, path: impl AsRef<Path>) -> Result<Option<Dir>>;
130
131 fn ensure_dir_with(
135 &self,
136 p: impl AsRef<Path>,
137 builder: &cap_std::fs::DirBuilder,
138 ) -> Result<bool>;
139
140 fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>>;
142
143 fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>>;
145
146 fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
148
149 fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
152
153 fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>>;
155
156 fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>>;
158
159 #[cfg(unix)]
161 fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()>;
162
163 #[cfg(not(windows))]
209 fn atomic_replace_with<F, T, E>(
210 &self,
211 destname: impl AsRef<Path>,
212 f: F,
213 ) -> std::result::Result<T, E>
214 where
215 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
216 E: From<std::io::Error>;
217
218 #[cfg(not(windows))]
220 fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()>;
221
222 #[cfg(not(windows))]
224 fn atomic_write_with_perms(
225 &self,
226 destname: impl AsRef<Path>,
227 contents: impl AsRef<[u8]>,
228 perms: cap_std::fs::Permissions,
229 ) -> Result<()>;
230
231 #[cfg(unix)]
237 fn reopen_as_ownedfd(&self) -> Result<OwnedFd>;
238
239 #[cfg(any(target_os = "android", target_os = "linux"))]
240 fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>>;
246
247 #[cfg(not(windows))]
248 fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>>;
251
252 #[cfg(not(windows))]
253 fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList>;
255
256 #[cfg(not(windows))]
257 fn setxattr(
259 &self,
260 path: impl AsRef<Path>,
261 key: impl AsRef<OsStr>,
262 value: impl AsRef<[u8]>,
263 ) -> Result<()>;
264
265 fn walk<C, E>(&self, config: &WalkConfiguration, callback: C) -> std::result::Result<(), E>
270 where
271 C: FnMut(&WalkComponent) -> WalkResult<E>,
272 E: From<std::io::Error>;
273}
274
275#[cfg(feature = "fs_utf8")]
276pub trait CapStdExtDirExtUtf8 {
280 fn open_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::File>>;
282
283 fn open_dir_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::Dir>>;
285
286 fn ensure_dir_with(
290 &self,
291 p: impl AsRef<Utf8Path>,
292 builder: &cap_std::fs::DirBuilder,
293 ) -> Result<bool>;
294
295 fn metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>>;
297
298 fn symlink_metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>>;
300
301 fn remove_file_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool>;
303
304 fn remove_all_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool>;
307
308 #[cfg(unix)]
310 fn update_timestamps(&self, path: impl AsRef<Utf8Path>) -> Result<()>;
311
312 fn atomic_replace_with<F, T, E>(
359 &self,
360 destname: impl AsRef<Utf8Path>,
361 f: F,
362 ) -> std::result::Result<T, E>
363 where
364 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
365 E: From<std::io::Error>;
366
367 fn atomic_write(
369 &self,
370 destname: impl AsRef<Utf8Path>,
371 contents: impl AsRef<[u8]>,
372 ) -> Result<()>;
373
374 fn atomic_write_with_perms(
376 &self,
377 destname: impl AsRef<Utf8Path>,
378 contents: impl AsRef<[u8]>,
379 perms: cap_std::fs::Permissions,
380 ) -> Result<()>;
381
382 fn filenames_sorted(&self) -> Result<Vec<String>> {
384 self.filenames_sorted_by(|a, b| a.cmp(b))
385 }
386
387 fn filenames_sorted_by<C>(&self, compare: C) -> Result<Vec<String>>
389 where
390 C: FnMut(&str, &str) -> std::cmp::Ordering,
391 {
392 self.filenames_filtered_sorted_by(|_, _| true, compare)
393 }
394
395 fn filenames_filtered_sorted<F>(&self, f: F) -> Result<Vec<String>>
397 where
398 F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
399 {
400 self.filenames_filtered_sorted_by(f, |a, b| a.cmp(b))
401 }
402
403 fn filenames_filtered_sorted_by<F, C>(&self, f: F, compare: C) -> Result<Vec<String>>
405 where
406 F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
407 C: FnMut(&str, &str) -> std::cmp::Ordering;
408}
409
410pub(crate) fn map_optional<R>(r: Result<R>) -> Result<Option<R>> {
411 match r {
412 Ok(v) => Ok(Some(v)),
413 Err(e) => {
414 if e.kind() == std::io::ErrorKind::NotFound {
415 Ok(None)
416 } else {
417 Err(e)
418 }
419 }
420 }
421}
422
423enum DirOwnedOrBorrowed<'d> {
424 Owned(Dir),
425 Borrowed(&'d Dir),
426}
427
428impl<'d> Deref for DirOwnedOrBorrowed<'d> {
429 type Target = Dir;
430
431 fn deref(&self) -> &Self::Target {
432 match self {
433 Self::Owned(d) => d,
434 Self::Borrowed(d) => d,
435 }
436 }
437}
438
439fn subdir_of<'d, 'p>(d: &'d Dir, p: &'p Path) -> io::Result<(DirOwnedOrBorrowed<'d>, &'p OsStr)> {
446 let name = p
447 .file_name()
448 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Not a file name"))?;
449 let r = if let Some(subdir) = p
450 .parent()
451 .filter(|v| !v.as_os_str().is_empty())
452 .map(|p| d.open_dir(p))
453 {
454 DirOwnedOrBorrowed::Owned(subdir?)
455 } else {
456 DirOwnedOrBorrowed::Borrowed(d)
457 };
458 Ok((r, name))
459}
460
461#[cfg(any(target_os = "android", target_os = "linux"))]
462fn openat2_with_retry(
464 dirfd: impl std::os::fd::AsFd,
465 path: impl AsRef<Path>,
466 oflags: rustix::fs::OFlags,
467 mode: rustix::fs::Mode,
468 resolve: rustix::fs::ResolveFlags,
469) -> rustix::io::Result<std::os::fd::OwnedFd> {
470 let dirfd = dirfd.as_fd();
471 let path = path.as_ref();
472 path.into_with_c_str(|path_c_str| 'start: loop {
475 match rustix::fs::openat2(dirfd, path_c_str, oflags, mode, resolve) {
476 Ok(file) => {
477 return Ok(file);
478 }
479 Err(rustix::io::Errno::AGAIN | rustix::io::Errno::INTR) => {
480 continue 'start;
481 }
482 Err(e) => {
483 return Err(e);
484 }
485 }
486 })
487}
488
489#[cfg(any(target_os = "android", target_os = "linux"))]
490fn is_mountpoint_impl_statx(root: &Dir, path: &Path) -> Result<Option<bool>> {
491 use rustix::fs::StatxAttributes;
493 use rustix::fs::{AtFlags, StatxFlags};
494 use std::os::fd::AsFd;
495
496 match rustix::fs::statx(
498 root.as_fd(),
499 path,
500 AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW,
501 StatxFlags::empty(),
502 ) {
503 Ok(r) => {
504 let present = r.stx_attributes_mask.contains(StatxAttributes::MOUNT_ROOT);
505 Ok(present.then_some(r.stx_attributes.contains(StatxAttributes::MOUNT_ROOT)))
506 }
507 Err(e) if e == rustix::io::Errno::NOSYS => Ok(None),
508 Err(e) => Err(e.into()),
509 }
510}
511
512#[cfg(any(target_os = "android", target_os = "linux"))]
514fn impl_open_dir_noxdev(
515 d: &Dir,
516 path: impl AsRef<std::path::Path>,
517) -> std::io::Result<Option<Dir>> {
518 use rustix::fs::{Mode, OFlags, ResolveFlags};
519 match openat2_with_retry(
520 d,
521 path,
522 OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::NOFOLLOW,
523 Mode::empty(),
524 ResolveFlags::NO_XDEV | ResolveFlags::BENEATH,
525 ) {
526 Ok(r) => Ok(Some(Dir::reopen_dir(&r)?)),
527 Err(e) if e == rustix::io::Errno::XDEV => Ok(None),
528 Err(e) => Err(e.into()),
529 }
530}
531
532fn walk_inner<E>(
534 d: &Dir,
535 path: &mut PathBuf,
536 callback: &mut dyn FnMut(&WalkComponent) -> WalkResult<E>,
537 config: &WalkConfiguration,
538) -> std::result::Result<(), E>
539where
540 E: From<std::io::Error>,
541{
542 let entries = d.entries()?;
543 let entries: Box<dyn Iterator<Item = Result<DirEntry>>> =
545 if let Some(sorter) = config.sorter.as_ref() {
546 let mut entries = entries.collect::<Result<Vec<_>>>()?;
547 entries.sort_by(|a, b| sorter(a, b));
548 Box::new(entries.into_iter().map(Ok))
549 } else {
550 Box::new(entries.into_iter())
551 };
552 for entry in entries {
554 let entry = &entry?;
555 let ty = entry.file_type()?;
557 let is_dir = ty.is_dir();
558 let name = entry.file_name();
559 path.push(&name);
561 let filename = &name;
562 let component = WalkComponent {
563 path,
564 dir: d,
565 filename,
566 file_type: ty,
567 entry,
568 };
569 let flow = callback(&component)?;
571 let is_break = matches!(flow, std::ops::ControlFlow::Break(()));
573 if !is_dir {
575 path.pop();
576 if is_break {
578 return Ok(());
579 } else {
580 continue;
582 }
583 } else if is_break {
584 path.pop();
586 continue;
587 }
588 debug_assert!(matches!(flow, std::ops::ControlFlow::Continue(())));
591 #[cfg(any(target_os = "android", target_os = "linux"))]
594 let d = {
595 if !config.noxdev {
596 entry.open_dir()?
597 } else if let Some(d) = impl_open_dir_noxdev(d, filename)? {
598 d
599 } else {
600 path.pop();
601 continue;
602 }
603 };
604
605 #[cfg(not(any(target_os = "android", target_os = "linux")))]
606 let d = entry.open_dir()?;
607
608 walk_inner(&d, path, callback, config)?;
610 path.pop();
611 }
612 Ok(())
613}
614
615pub(crate) fn validate_relpath_no_uplinks(path: &Path) -> Result<&Path> {
618 let is_absolute = path.is_absolute();
619 let contains_uplinks = path
620 .components()
621 .any(|e| e == std::path::Component::ParentDir);
622 if is_absolute || contains_uplinks {
623 Err(crate::escape_attempt())
624 } else {
625 Ok(path)
626 }
627}
628
629impl CapStdExtDirExt for Dir {
630 fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>> {
631 map_optional(self.open(path.as_ref()))
632 }
633
634 fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>> {
635 map_optional(self.open_dir(path.as_ref()))
636 }
637
638 #[cfg(any(target_os = "android", target_os = "linux"))]
639 fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir> {
640 crate::RootDir::new(self, path)
641 }
642
643 #[cfg(any(target_os = "android", target_os = "linux"))]
645 fn open_dir_noxdev(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<Dir>> {
646 impl_open_dir_noxdev(self, path)
647 }
648
649 fn ensure_dir_with(
650 &self,
651 p: impl AsRef<Path>,
652 builder: &cap_std::fs::DirBuilder,
653 ) -> Result<bool> {
654 let p = p.as_ref();
655 match self.create_dir_with(p, builder) {
656 Ok(()) => Ok(true),
657 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
658 if !self.symlink_metadata(p)?.is_dir() {
659 return Err(io::Error::new(io::ErrorKind::Other, "Found non-directory"));
662 }
663 Ok(false)
664 }
665 Err(e) => Err(e),
666 }
667 }
668
669 fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
670 map_optional(self.metadata(path.as_ref()))
671 }
672
673 fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
674 map_optional(self.symlink_metadata(path.as_ref()))
675 }
676
677 fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
678 match self.remove_file(path.as_ref()) {
679 Ok(()) => Ok(true),
680 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
681 Err(e) => Err(e),
682 }
683 }
684
685 fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
686 let path = path.as_ref();
687 let meta = match self.symlink_metadata_optional(path)? {
691 Some(m) => m,
692 None => return Ok(false),
693 };
694 if meta.is_dir() {
695 self.remove_dir_all(path)?;
696 } else {
697 self.remove_file(path)?;
698 }
699 Ok(true)
700 }
701
702 fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>> {
703 let mut r = Vec::new();
704 let Some(mut f) = self.open_optional(path.as_ref())? else {
705 return Ok(None);
706 };
707 f.read_to_end(&mut r)?;
708 Ok(Some(r))
709 }
710
711 fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>> {
712 let mut r = String::new();
713 let Some(mut f) = self.open_optional(path.as_ref())? else {
714 return Ok(None);
715 };
716 f.read_to_string(&mut r)?;
717 Ok(Some(r))
718 }
719
720 #[cfg(unix)]
721 fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()> {
722 use rustix::fd::AsFd;
723 use rustix::fs::UTIME_NOW;
724
725 let path = path.as_ref();
726 let now = rustix::fs::Timespec {
727 tv_sec: 0,
728 tv_nsec: UTIME_NOW,
729 };
730 #[allow(clippy::clone_on_copy)]
733 let times = rustix::fs::Timestamps {
734 last_access: now.clone(),
735 last_modification: now.clone(),
736 };
737 rustix::fs::utimensat(
738 self.as_fd(),
739 path,
740 ×,
741 rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
742 )?;
743 Ok(())
744 }
745
746 #[cfg(not(windows))]
747 fn atomic_replace_with<F, T, E>(
748 &self,
749 destname: impl AsRef<Path>,
750 f: F,
751 ) -> std::result::Result<T, E>
752 where
753 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
754 E: From<std::io::Error>,
755 {
756 let destname = destname.as_ref();
757 let (d, name) = subdir_of(self, destname)?;
758 let existing_metadata = d.symlink_metadata_optional(destname)?;
759 let existing_perms = existing_metadata
762 .filter(|m| m.is_file())
763 .map(|m| m.permissions());
764 let mut t = cap_tempfile::TempFile::new(&d)?;
765 if let Some(existing_perms) = existing_perms {
767 t.as_file_mut().set_permissions(existing_perms)?;
768 }
769 let mut bufw = std::io::BufWriter::new(t);
771 let r = f(&mut bufw)?;
773 t = bufw.into_inner().map_err(From::from)?;
775 t.as_file().sync_all()?;
777 t.replace(name)?;
779 d.open(".")?.sync_all()?;
781 Ok(r)
782 }
783
784 #[cfg(not(windows))]
785 fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
786 self.atomic_replace_with(destname, |f| f.write_all(contents.as_ref()))
787 }
788
789 #[cfg(not(windows))]
790 fn atomic_write_with_perms(
791 &self,
792 destname: impl AsRef<Path>,
793 contents: impl AsRef<[u8]>,
794 perms: cap_std::fs::Permissions,
795 ) -> Result<()> {
796 self.atomic_replace_with(destname, |f| -> io::Result<_> {
797 #[cfg(unix)]
801 {
802 use cap_std::fs::PermissionsExt;
803 let perms = cap_std::fs::Permissions::from_mode(0o600);
804 f.get_mut().as_file_mut().set_permissions(perms)?;
805 }
806 f.write_all(contents.as_ref())?;
807 f.flush()?;
808 f.get_mut().as_file_mut().set_permissions(perms)?;
809 Ok(())
810 })
811 }
812
813 #[cfg(unix)]
814 fn reopen_as_ownedfd(&self) -> Result<OwnedFd> {
815 use rustix::fs::{Mode, OFlags};
816 use std::os::fd::AsFd;
817 rustix::fs::openat(
818 self.as_fd(),
819 ".",
820 OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::RDONLY,
821 Mode::empty(),
822 )
823 .map_err(Into::into)
824 }
825
826 #[cfg(any(target_os = "android", target_os = "linux"))]
827 fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>> {
828 is_mountpoint_impl_statx(self, path.as_ref()).map_err(Into::into)
829 }
830
831 #[cfg(not(windows))]
832 fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>> {
833 crate::xattrs::impl_getxattr(self, path.as_ref(), key.as_ref())
834 }
835
836 #[cfg(not(windows))]
837 fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList> {
838 crate::xattrs::impl_listxattrs(self, path.as_ref())
839 }
840
841 #[cfg(not(windows))]
842 fn setxattr(
843 &self,
844 path: impl AsRef<Path>,
845 key: impl AsRef<OsStr>,
846 value: impl AsRef<[u8]>,
847 ) -> Result<()> {
848 crate::xattrs::impl_setxattr(self, path.as_ref(), key.as_ref(), value.as_ref())
849 }
850
851 fn walk<C, E>(&self, config: &WalkConfiguration, mut callback: C) -> std::result::Result<(), E>
852 where
853 C: FnMut(&WalkComponent) -> WalkResult<E>,
854 E: From<std::io::Error>,
855 {
856 let mut pb = config
857 .path_base
858 .as_ref()
859 .map(|v| v.to_path_buf())
860 .unwrap_or_default();
861 walk_inner(self, &mut pb, &mut callback, config)
862 }
863}
864
865#[cfg(feature = "fs_utf8")]
869impl CapStdExtDirExtUtf8 for cap_std::fs_utf8::Dir {
870 fn open_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::File>> {
871 map_optional(self.open(path.as_ref()))
872 }
873
874 fn open_dir_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::Dir>> {
875 map_optional(self.open_dir(path.as_ref()))
876 }
877
878 fn ensure_dir_with(
879 &self,
880 p: impl AsRef<Utf8Path>,
881 builder: &cap_std::fs::DirBuilder,
882 ) -> Result<bool> {
883 self.as_cap_std()
884 .ensure_dir_with(p.as_ref().as_std_path(), builder)
885 }
886
887 fn metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>> {
888 self.as_cap_std()
889 .metadata_optional(path.as_ref().as_std_path())
890 }
891
892 fn symlink_metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>> {
893 self.as_cap_std()
894 .symlink_metadata_optional(path.as_ref().as_std_path())
895 }
896
897 fn remove_file_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool> {
898 self.as_cap_std()
899 .remove_file_optional(path.as_ref().as_std_path())
900 }
901
902 fn remove_all_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool> {
903 self.as_cap_std()
904 .remove_all_optional(path.as_ref().as_std_path())
905 }
906
907 #[cfg(unix)]
908 fn update_timestamps(&self, path: impl AsRef<Utf8Path>) -> Result<()> {
909 self.as_cap_std()
910 .update_timestamps(path.as_ref().as_std_path())
911 }
912
913 fn atomic_replace_with<F, T, E>(
914 &self,
915 destname: impl AsRef<Utf8Path>,
916 f: F,
917 ) -> std::result::Result<T, E>
918 where
919 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
920 E: From<std::io::Error>,
921 {
922 self.as_cap_std()
923 .atomic_replace_with(destname.as_ref().as_std_path(), f)
924 }
925
926 fn atomic_write(
927 &self,
928 destname: impl AsRef<Utf8Path>,
929 contents: impl AsRef<[u8]>,
930 ) -> Result<()> {
931 self.as_cap_std()
932 .atomic_write(destname.as_ref().as_std_path(), contents)
933 }
934
935 fn atomic_write_with_perms(
936 &self,
937 destname: impl AsRef<Utf8Path>,
938 contents: impl AsRef<[u8]>,
939 perms: cap_std::fs::Permissions,
940 ) -> Result<()> {
941 self.as_cap_std()
942 .atomic_write_with_perms(destname.as_ref().as_std_path(), contents, perms)
943 }
944
945 fn filenames_filtered_sorted_by<F, C>(&self, mut f: F, mut compare: C) -> Result<Vec<String>>
946 where
947 F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
948 C: FnMut(&str, &str) -> std::cmp::Ordering,
949 {
950 let mut r =
951 self.entries()?
952 .try_fold(Vec::new(), |mut acc, ent| -> Result<Vec<String>> {
953 let ent = ent?;
954 let name = ent.file_name()?;
955 if f(&ent, name.as_str()) {
956 acc.push(name);
957 }
958 Ok(acc)
959 })?;
960 r.sort_by(|a, b| compare(a.as_str(), b.as_str()));
961 Ok(r)
962 }
963}
964
965#[cfg(test)]
966mod tests {
967 use std::path::Path;
968
969 use super::*;
970
971 #[test]
972 fn test_validate_relpath_no_uplinks() {
973 let ok_cases = ["foo", "foo/bar", "foo/bar/"];
974 #[cfg(unix)]
975 let err_cases = ["/foo", "/", "../foo", "foo/../bar"];
976 #[cfg(windows)]
977 let err_cases = ["C:\\foo", "../foo", "foo/../bar"];
978
979 for case in ok_cases {
980 assert!(validate_relpath_no_uplinks(Path::new(case)).is_ok());
981 }
982 for case in err_cases {
983 assert!(validate_relpath_no_uplinks(Path::new(case)).is_err());
984 }
985 }
986}