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(any(target_os = "android", target_os = "linux"))]
248 fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>>;
251
252 #[cfg(any(target_os = "android", target_os = "linux"))]
253 fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList>;
255
256 #[cfg(any(target_os = "android", target_os = "linux"))]
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 Deref for DirOwnedOrBorrowed<'_> {
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
615#[cfg_attr(
618 not(any(target_os = "android", target_os = "linux", test)),
619 allow(dead_code)
620)]
621pub(crate) fn validate_relpath_no_uplinks(path: &Path) -> Result<&Path> {
622 let is_absolute = path.is_absolute();
623 let contains_uplinks = path
624 .components()
625 .any(|e| e == std::path::Component::ParentDir);
626 if is_absolute || contains_uplinks {
627 Err(crate::escape_attempt())
628 } else {
629 Ok(path)
630 }
631}
632
633impl CapStdExtDirExt for Dir {
634 fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>> {
635 map_optional(self.open(path.as_ref()))
636 }
637
638 fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>> {
639 map_optional(self.open_dir(path.as_ref()))
640 }
641
642 #[cfg(any(target_os = "android", target_os = "linux"))]
643 fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir> {
644 crate::RootDir::new(self, path)
645 }
646
647 #[cfg(any(target_os = "android", target_os = "linux"))]
649 fn open_dir_noxdev(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<Dir>> {
650 impl_open_dir_noxdev(self, path)
651 }
652
653 fn ensure_dir_with(
654 &self,
655 p: impl AsRef<Path>,
656 builder: &cap_std::fs::DirBuilder,
657 ) -> Result<bool> {
658 let p = p.as_ref();
659 match self.create_dir_with(p, builder) {
660 Ok(()) => Ok(true),
661 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
662 if !self.symlink_metadata(p)?.is_dir() {
663 return Err(io::Error::other("Found non-directory"));
666 }
667 Ok(false)
668 }
669 Err(e) => Err(e),
670 }
671 }
672
673 fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
674 map_optional(self.metadata(path.as_ref()))
675 }
676
677 fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
678 map_optional(self.symlink_metadata(path.as_ref()))
679 }
680
681 fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
682 match self.remove_file(path.as_ref()) {
683 Ok(()) => Ok(true),
684 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
685 Err(e) => Err(e),
686 }
687 }
688
689 fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
690 let path = path.as_ref();
691 let meta = match self.symlink_metadata_optional(path)? {
695 Some(m) => m,
696 None => return Ok(false),
697 };
698 if meta.is_dir() {
699 self.remove_dir_all(path)?;
700 } else {
701 self.remove_file(path)?;
702 }
703 Ok(true)
704 }
705
706 fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>> {
707 let mut r = Vec::new();
708 let Some(mut f) = self.open_optional(path.as_ref())? else {
709 return Ok(None);
710 };
711 f.read_to_end(&mut r)?;
712 Ok(Some(r))
713 }
714
715 fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>> {
716 let mut r = String::new();
717 let Some(mut f) = self.open_optional(path.as_ref())? else {
718 return Ok(None);
719 };
720 f.read_to_string(&mut r)?;
721 Ok(Some(r))
722 }
723
724 #[cfg(unix)]
725 fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()> {
726 use rustix::fd::AsFd;
727 use rustix::fs::UTIME_NOW;
728
729 let path = path.as_ref();
730 let now = rustix::fs::Timespec {
731 tv_sec: 0,
732 tv_nsec: UTIME_NOW,
733 };
734 #[allow(clippy::clone_on_copy)]
737 let times = rustix::fs::Timestamps {
738 last_access: now.clone(),
739 last_modification: now.clone(),
740 };
741 rustix::fs::utimensat(
742 self.as_fd(),
743 path,
744 ×,
745 rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
746 )?;
747 Ok(())
748 }
749
750 #[cfg(not(windows))]
751 fn atomic_replace_with<F, T, E>(
752 &self,
753 destname: impl AsRef<Path>,
754 f: F,
755 ) -> std::result::Result<T, E>
756 where
757 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
758 E: From<std::io::Error>,
759 {
760 let destname = destname.as_ref();
761 let (d, name) = subdir_of(self, destname)?;
762 let existing_metadata = d.symlink_metadata_optional(destname)?;
763 let existing_perms = existing_metadata
766 .filter(|m| m.is_file())
767 .map(|m| m.permissions());
768 let mut t = cap_tempfile::TempFile::new(&d)?;
769 if let Some(existing_perms) = existing_perms {
771 t.as_file_mut().set_permissions(existing_perms)?;
772 }
773 let mut bufw = std::io::BufWriter::new(t);
775 let r = f(&mut bufw)?;
777 t = bufw.into_inner().map_err(From::from)?;
779 t.as_file().sync_all()?;
781 t.replace(name)?;
783 d.open(".")?.sync_all()?;
785 Ok(r)
786 }
787
788 #[cfg(not(windows))]
789 fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
790 self.atomic_replace_with(destname, |f| f.write_all(contents.as_ref()))
791 }
792
793 #[cfg(not(windows))]
794 fn atomic_write_with_perms(
795 &self,
796 destname: impl AsRef<Path>,
797 contents: impl AsRef<[u8]>,
798 perms: cap_std::fs::Permissions,
799 ) -> Result<()> {
800 self.atomic_replace_with(destname, |f| -> io::Result<_> {
801 #[cfg(unix)]
805 {
806 use cap_std::fs::PermissionsExt;
807 let perms = cap_std::fs::Permissions::from_mode(0o600);
808 f.get_mut().as_file_mut().set_permissions(perms)?;
809 }
810 f.write_all(contents.as_ref())?;
811 f.flush()?;
812 f.get_mut().as_file_mut().set_permissions(perms)?;
813 Ok(())
814 })
815 }
816
817 #[cfg(unix)]
818 fn reopen_as_ownedfd(&self) -> Result<OwnedFd> {
819 use rustix::fs::{Mode, OFlags};
820 use std::os::fd::AsFd;
821 rustix::fs::openat(
822 self.as_fd(),
823 ".",
824 OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::RDONLY,
825 Mode::empty(),
826 )
827 .map_err(Into::into)
828 }
829
830 #[cfg(any(target_os = "android", target_os = "linux"))]
831 fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>> {
832 is_mountpoint_impl_statx(self, path.as_ref())
833 }
834
835 #[cfg(any(target_os = "android", target_os = "linux"))]
836 fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>> {
837 crate::xattrs::impl_getxattr(self, path.as_ref(), key.as_ref())
838 }
839
840 #[cfg(any(target_os = "android", target_os = "linux"))]
841 fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList> {
842 crate::xattrs::impl_listxattrs(self, path.as_ref())
843 }
844
845 #[cfg(any(target_os = "android", target_os = "linux"))]
846 fn setxattr(
847 &self,
848 path: impl AsRef<Path>,
849 key: impl AsRef<OsStr>,
850 value: impl AsRef<[u8]>,
851 ) -> Result<()> {
852 crate::xattrs::impl_setxattr(self, path.as_ref(), key.as_ref(), value.as_ref())
853 }
854
855 fn walk<C, E>(&self, config: &WalkConfiguration, mut callback: C) -> std::result::Result<(), E>
856 where
857 C: FnMut(&WalkComponent) -> WalkResult<E>,
858 E: From<std::io::Error>,
859 {
860 let mut pb = config
861 .path_base
862 .as_ref()
863 .map(|v| v.to_path_buf())
864 .unwrap_or_default();
865 walk_inner(self, &mut pb, &mut callback, config)
866 }
867}
868
869#[cfg(feature = "fs_utf8")]
873impl CapStdExtDirExtUtf8 for cap_std::fs_utf8::Dir {
874 fn open_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::File>> {
875 map_optional(self.open(path.as_ref()))
876 }
877
878 fn open_dir_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::Dir>> {
879 map_optional(self.open_dir(path.as_ref()))
880 }
881
882 fn ensure_dir_with(
883 &self,
884 p: impl AsRef<Utf8Path>,
885 builder: &cap_std::fs::DirBuilder,
886 ) -> Result<bool> {
887 self.as_cap_std()
888 .ensure_dir_with(p.as_ref().as_std_path(), builder)
889 }
890
891 fn metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>> {
892 self.as_cap_std()
893 .metadata_optional(path.as_ref().as_std_path())
894 }
895
896 fn symlink_metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>> {
897 self.as_cap_std()
898 .symlink_metadata_optional(path.as_ref().as_std_path())
899 }
900
901 fn remove_file_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool> {
902 self.as_cap_std()
903 .remove_file_optional(path.as_ref().as_std_path())
904 }
905
906 fn remove_all_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool> {
907 self.as_cap_std()
908 .remove_all_optional(path.as_ref().as_std_path())
909 }
910
911 #[cfg(unix)]
912 fn update_timestamps(&self, path: impl AsRef<Utf8Path>) -> Result<()> {
913 self.as_cap_std()
914 .update_timestamps(path.as_ref().as_std_path())
915 }
916
917 fn atomic_replace_with<F, T, E>(
918 &self,
919 destname: impl AsRef<Utf8Path>,
920 f: F,
921 ) -> std::result::Result<T, E>
922 where
923 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
924 E: From<std::io::Error>,
925 {
926 self.as_cap_std()
927 .atomic_replace_with(destname.as_ref().as_std_path(), f)
928 }
929
930 fn atomic_write(
931 &self,
932 destname: impl AsRef<Utf8Path>,
933 contents: impl AsRef<[u8]>,
934 ) -> Result<()> {
935 self.as_cap_std()
936 .atomic_write(destname.as_ref().as_std_path(), contents)
937 }
938
939 fn atomic_write_with_perms(
940 &self,
941 destname: impl AsRef<Utf8Path>,
942 contents: impl AsRef<[u8]>,
943 perms: cap_std::fs::Permissions,
944 ) -> Result<()> {
945 self.as_cap_std()
946 .atomic_write_with_perms(destname.as_ref().as_std_path(), contents, perms)
947 }
948
949 fn filenames_filtered_sorted_by<F, C>(&self, mut f: F, mut compare: C) -> Result<Vec<String>>
950 where
951 F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
952 C: FnMut(&str, &str) -> std::cmp::Ordering,
953 {
954 let mut r =
955 self.entries()?
956 .try_fold(Vec::new(), |mut acc, ent| -> Result<Vec<String>> {
957 let ent = ent?;
958 let name = ent.file_name()?;
959 if f(&ent, name.as_str()) {
960 acc.push(name);
961 }
962 Ok(acc)
963 })?;
964 r.sort_by(|a, b| compare(a.as_str(), b.as_str()));
965 Ok(r)
966 }
967}
968
969#[cfg(test)]
970mod tests {
971 use std::path::Path;
972
973 use super::*;
974
975 #[test]
976 fn test_validate_relpath_no_uplinks() {
977 let ok_cases = ["foo", "foo/bar", "foo/bar/"];
978 #[cfg(unix)]
979 let err_cases = ["/foo", "/", "../foo", "foo/../bar"];
980 #[cfg(windows)]
981 let err_cases = ["C:\\foo", "../foo", "foo/../bar"];
982
983 for case in ok_cases {
984 assert!(validate_relpath_no_uplinks(Path::new(case)).is_ok());
985 }
986 for case in err_cases {
987 assert!(validate_relpath_no_uplinks(Path::new(case)).is_err());
988 }
989 }
990}