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 skip_mountpoints: bool,
64
65 path_base: Option<&'p Path>,
66
67 #[allow(clippy::type_complexity)]
69 sorter: Option<Box<dyn Fn(&DirEntry, &DirEntry) -> Ordering + 'static>>,
70}
71
72pub type WalkResult<E> = std::result::Result<std::ops::ControlFlow<()>, E>;
74
75impl std::fmt::Debug for WalkConfiguration<'_> {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("WalkConfiguration")
78 .field("noxdev", &self.noxdev)
79 .field("skip_mountpoints", &self.skip_mountpoints)
80 .field("sorter", &self.sorter.as_ref().map(|_| true))
81 .finish()
82 }
83}
84
85impl<'p> WalkConfiguration<'p> {
86 pub fn noxdev(mut self) -> Self {
92 self.noxdev = true;
93 self
94 }
95
96 pub fn skip_mountpoints(mut self) -> Self {
108 self.skip_mountpoints = true;
109 self
110 }
111
112 pub fn sort_by<F>(mut self, cmp: F) -> Self
114 where
115 F: Fn(&DirEntry, &DirEntry) -> Ordering + 'static,
116 {
117 self.sorter = Some(Box::new(cmp));
118 self
119 }
120
121 pub fn sort_by_file_name(self) -> Self {
123 self.sort_by(|a, b| a.file_name().cmp(&b.file_name()))
124 }
125
126 pub fn path_base(mut self, base: &'p Path) -> Self {
131 self.path_base = Some(base);
132 self
133 }
134}
135
136pub trait CapStdExtDirExt {
140 fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>>;
142
143 fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>>;
145
146 #[cfg(any(target_os = "android", target_os = "linux"))]
149 fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir>;
150
151 #[cfg(any(target_os = "android", target_os = "linux"))]
153 fn open_dir_noxdev(&self, path: impl AsRef<Path>) -> Result<Option<Dir>>;
154
155 fn ensure_dir_with(
159 &self,
160 p: impl AsRef<Path>,
161 builder: &cap_std::fs::DirBuilder,
162 ) -> Result<bool>;
163
164 fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>>;
166
167 fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>>;
169
170 fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
172
173 fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
176
177 fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>>;
179
180 fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>>;
182
183 #[cfg(unix)]
185 fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()>;
186
187 #[cfg(not(windows))]
233 fn atomic_replace_with<F, T, E>(
234 &self,
235 destname: impl AsRef<Path>,
236 f: F,
237 ) -> std::result::Result<T, E>
238 where
239 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
240 E: From<std::io::Error>;
241
242 #[cfg(not(windows))]
244 fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()>;
245
246 #[cfg(not(windows))]
248 fn atomic_write_with_perms(
249 &self,
250 destname: impl AsRef<Path>,
251 contents: impl AsRef<[u8]>,
252 perms: cap_std::fs::Permissions,
253 ) -> Result<()>;
254
255 #[cfg(unix)]
261 fn reopen_as_ownedfd(&self) -> Result<OwnedFd>;
262
263 #[cfg(any(target_os = "android", target_os = "linux"))]
264 fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>>;
270
271 #[cfg(any(target_os = "android", target_os = "linux"))]
272 fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>>;
275
276 #[cfg(any(target_os = "android", target_os = "linux"))]
277 fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList>;
279
280 #[cfg(any(target_os = "android", target_os = "linux"))]
281 fn setxattr(
283 &self,
284 path: impl AsRef<Path>,
285 key: impl AsRef<OsStr>,
286 value: impl AsRef<[u8]>,
287 ) -> Result<()>;
288
289 fn walk<C, E>(&self, config: &WalkConfiguration, callback: C) -> std::result::Result<(), E>
294 where
295 C: FnMut(&WalkComponent) -> WalkResult<E>,
296 E: From<std::io::Error>;
297}
298
299#[cfg(feature = "fs_utf8")]
300pub trait CapStdExtDirExtUtf8 {
304 fn open_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::File>>;
306
307 fn open_dir_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::Dir>>;
309
310 fn ensure_dir_with(
314 &self,
315 p: impl AsRef<Utf8Path>,
316 builder: &cap_std::fs::DirBuilder,
317 ) -> Result<bool>;
318
319 fn metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>>;
321
322 fn symlink_metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>>;
324
325 fn remove_file_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool>;
327
328 fn remove_all_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool>;
331
332 #[cfg(unix)]
334 fn update_timestamps(&self, path: impl AsRef<Utf8Path>) -> Result<()>;
335
336 fn atomic_replace_with<F, T, E>(
383 &self,
384 destname: impl AsRef<Utf8Path>,
385 f: F,
386 ) -> std::result::Result<T, E>
387 where
388 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
389 E: From<std::io::Error>;
390
391 fn atomic_write(
393 &self,
394 destname: impl AsRef<Utf8Path>,
395 contents: impl AsRef<[u8]>,
396 ) -> Result<()>;
397
398 fn atomic_write_with_perms(
400 &self,
401 destname: impl AsRef<Utf8Path>,
402 contents: impl AsRef<[u8]>,
403 perms: cap_std::fs::Permissions,
404 ) -> Result<()>;
405
406 fn filenames_sorted(&self) -> Result<Vec<String>> {
408 self.filenames_sorted_by(|a, b| a.cmp(b))
409 }
410
411 fn filenames_sorted_by<C>(&self, compare: C) -> Result<Vec<String>>
413 where
414 C: FnMut(&str, &str) -> std::cmp::Ordering,
415 {
416 self.filenames_filtered_sorted_by(|_, _| true, compare)
417 }
418
419 fn filenames_filtered_sorted<F>(&self, f: F) -> Result<Vec<String>>
421 where
422 F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
423 {
424 self.filenames_filtered_sorted_by(f, |a, b| a.cmp(b))
425 }
426
427 fn filenames_filtered_sorted_by<F, C>(&self, f: F, compare: C) -> Result<Vec<String>>
429 where
430 F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
431 C: FnMut(&str, &str) -> std::cmp::Ordering;
432}
433
434pub(crate) fn map_optional<R>(r: Result<R>) -> Result<Option<R>> {
435 match r {
436 Ok(v) => Ok(Some(v)),
437 Err(e) => {
438 if e.kind() == std::io::ErrorKind::NotFound {
439 Ok(None)
440 } else {
441 Err(e)
442 }
443 }
444 }
445}
446
447enum DirOwnedOrBorrowed<'d> {
448 Owned(Dir),
449 Borrowed(&'d Dir),
450}
451
452impl Deref for DirOwnedOrBorrowed<'_> {
453 type Target = Dir;
454
455 fn deref(&self) -> &Self::Target {
456 match self {
457 Self::Owned(d) => d,
458 Self::Borrowed(d) => d,
459 }
460 }
461}
462
463fn subdir_of<'d, 'p>(d: &'d Dir, p: &'p Path) -> io::Result<(DirOwnedOrBorrowed<'d>, &'p OsStr)> {
470 let name = p
471 .file_name()
472 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Not a file name"))?;
473 let r = if let Some(subdir) = p
474 .parent()
475 .filter(|v| !v.as_os_str().is_empty())
476 .map(|p| d.open_dir(p))
477 {
478 DirOwnedOrBorrowed::Owned(subdir?)
479 } else {
480 DirOwnedOrBorrowed::Borrowed(d)
481 };
482 Ok((r, name))
483}
484
485#[cfg(any(target_os = "android", target_os = "linux"))]
486fn openat2_with_retry(
488 dirfd: impl std::os::fd::AsFd,
489 path: impl AsRef<Path>,
490 oflags: rustix::fs::OFlags,
491 mode: rustix::fs::Mode,
492 resolve: rustix::fs::ResolveFlags,
493) -> rustix::io::Result<std::os::fd::OwnedFd> {
494 let dirfd = dirfd.as_fd();
495 let path = path.as_ref();
496 path.into_with_c_str(|path_c_str| 'start: loop {
499 match rustix::fs::openat2(dirfd, path_c_str, oflags, mode, resolve) {
500 Ok(file) => {
501 return Ok(file);
502 }
503 Err(rustix::io::Errno::AGAIN | rustix::io::Errno::INTR) => {
504 continue 'start;
505 }
506 Err(e) => {
507 return Err(e);
508 }
509 }
510 })
511}
512
513#[cfg(any(target_os = "android", target_os = "linux"))]
514fn is_mountpoint_impl_statx(root: &Dir, path: &Path) -> Result<Option<bool>> {
515 use rustix::fs::StatxAttributes;
517 use rustix::fs::{AtFlags, StatxFlags};
518 use std::os::fd::AsFd;
519
520 match rustix::fs::statx(
522 root.as_fd(),
523 path,
524 AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW,
525 StatxFlags::empty(),
526 ) {
527 Ok(r) => {
528 let present = r.stx_attributes_mask.contains(StatxAttributes::MOUNT_ROOT);
529 Ok(present.then_some(r.stx_attributes.contains(StatxAttributes::MOUNT_ROOT)))
530 }
531 Err(e) if e == rustix::io::Errno::NOSYS => Ok(None),
532 Err(e) => Err(e.into()),
533 }
534}
535
536#[cfg(any(target_os = "android", target_os = "linux"))]
538fn impl_open_dir_noxdev(
539 d: &Dir,
540 path: impl AsRef<std::path::Path>,
541) -> std::io::Result<Option<Dir>> {
542 use rustix::fs::{Mode, OFlags, ResolveFlags};
543 match openat2_with_retry(
544 d,
545 path,
546 OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::NOFOLLOW,
547 Mode::empty(),
548 ResolveFlags::NO_XDEV | ResolveFlags::BENEATH,
549 ) {
550 Ok(r) => Ok(Some(Dir::reopen_dir(&r)?)),
551 Err(e) if e == rustix::io::Errno::XDEV => Ok(None),
552 Err(e) => Err(e.into()),
553 }
554}
555
556fn walk_inner<E>(
558 d: &Dir,
559 path: &mut PathBuf,
560 callback: &mut dyn FnMut(&WalkComponent) -> WalkResult<E>,
561 config: &WalkConfiguration,
562) -> std::result::Result<(), E>
563where
564 E: From<std::io::Error>,
565{
566 let entries = d.entries()?;
567 let entries: Box<dyn Iterator<Item = Result<DirEntry>>> =
569 if let Some(sorter) = config.sorter.as_ref() {
570 let mut entries = entries.collect::<Result<Vec<_>>>()?;
571 entries.sort_by(|a, b| sorter(a, b));
572 Box::new(entries.into_iter().map(Ok))
573 } else {
574 Box::new(entries.into_iter())
575 };
576 for entry in entries {
578 let entry = &entry?;
579 let ty = entry.file_type()?;
581 let is_dir = ty.is_dir();
582 let name = entry.file_name();
583 #[cfg(any(target_os = "android", target_os = "linux"))]
586 if is_dir && config.skip_mountpoints {
587 if let Some(true) = is_mountpoint_impl_statx(d, Path::new(&name))? {
588 continue;
589 }
590 }
591 path.push(&name);
593 let filename = &name;
594 let component = WalkComponent {
595 path,
596 dir: d,
597 filename,
598 file_type: ty,
599 entry,
600 };
601 let flow = callback(&component)?;
603 let is_break = matches!(flow, std::ops::ControlFlow::Break(()));
605 if !is_dir {
607 path.pop();
608 if is_break {
610 return Ok(());
611 } else {
612 continue;
614 }
615 } else if is_break {
616 path.pop();
618 continue;
619 }
620 debug_assert!(matches!(flow, std::ops::ControlFlow::Continue(())));
623 #[cfg(any(target_os = "android", target_os = "linux"))]
626 let d = {
627 if !config.noxdev {
628 entry.open_dir()?
629 } else if let Some(d) = impl_open_dir_noxdev(d, filename)? {
630 d
631 } else {
632 path.pop();
633 continue;
634 }
635 };
636
637 #[cfg(not(any(target_os = "android", target_os = "linux")))]
638 let d = entry.open_dir()?;
639
640 walk_inner(&d, path, callback, config)?;
642 path.pop();
643 }
644 Ok(())
645}
646
647#[cfg_attr(
650 not(any(target_os = "android", target_os = "linux", test)),
651 allow(dead_code)
652)]
653pub(crate) fn validate_relpath_no_uplinks(path: &Path) -> Result<&Path> {
654 let is_absolute = path.is_absolute();
655 let contains_uplinks = path
656 .components()
657 .any(|e| e == std::path::Component::ParentDir);
658 if is_absolute || contains_uplinks {
659 Err(crate::escape_attempt())
660 } else {
661 Ok(path)
662 }
663}
664
665impl CapStdExtDirExt for Dir {
666 fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>> {
667 map_optional(self.open(path.as_ref()))
668 }
669
670 fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>> {
671 map_optional(self.open_dir(path.as_ref()))
672 }
673
674 #[cfg(any(target_os = "android", target_os = "linux"))]
675 fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir> {
676 crate::RootDir::new(self, path)
677 }
678
679 #[cfg(any(target_os = "android", target_os = "linux"))]
681 fn open_dir_noxdev(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<Dir>> {
682 impl_open_dir_noxdev(self, path)
683 }
684
685 fn ensure_dir_with(
686 &self,
687 p: impl AsRef<Path>,
688 builder: &cap_std::fs::DirBuilder,
689 ) -> Result<bool> {
690 let p = p.as_ref();
691 match self.create_dir_with(p, builder) {
692 Ok(()) => Ok(true),
693 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
694 if !self.symlink_metadata(p)?.is_dir() {
695 return Err(io::Error::other("Found non-directory"));
698 }
699 Ok(false)
700 }
701 Err(e) => Err(e),
702 }
703 }
704
705 fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
706 map_optional(self.metadata(path.as_ref()))
707 }
708
709 fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
710 map_optional(self.symlink_metadata(path.as_ref()))
711 }
712
713 fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
714 match self.remove_file(path.as_ref()) {
715 Ok(()) => Ok(true),
716 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
717 Err(e) => Err(e),
718 }
719 }
720
721 fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
722 let path = path.as_ref();
723 let meta = match self.symlink_metadata_optional(path)? {
727 Some(m) => m,
728 None => return Ok(false),
729 };
730 if meta.is_dir() {
731 self.remove_dir_all(path)?;
732 } else {
733 self.remove_file(path)?;
734 }
735 Ok(true)
736 }
737
738 fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>> {
739 let mut r = Vec::new();
740 let Some(mut f) = self.open_optional(path.as_ref())? else {
741 return Ok(None);
742 };
743 f.read_to_end(&mut r)?;
744 Ok(Some(r))
745 }
746
747 fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>> {
748 let mut r = String::new();
749 let Some(mut f) = self.open_optional(path.as_ref())? else {
750 return Ok(None);
751 };
752 f.read_to_string(&mut r)?;
753 Ok(Some(r))
754 }
755
756 #[cfg(unix)]
757 fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()> {
758 use rustix::fd::AsFd;
759 use rustix::fs::UTIME_NOW;
760
761 let path = path.as_ref();
762 let now = rustix::fs::Timespec {
763 tv_sec: 0,
764 tv_nsec: UTIME_NOW,
765 };
766 #[allow(clippy::clone_on_copy)]
769 let times = rustix::fs::Timestamps {
770 last_access: now.clone(),
771 last_modification: now.clone(),
772 };
773 rustix::fs::utimensat(
774 self.as_fd(),
775 path,
776 ×,
777 rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
778 )?;
779 Ok(())
780 }
781
782 #[cfg(not(windows))]
783 fn atomic_replace_with<F, T, E>(
784 &self,
785 destname: impl AsRef<Path>,
786 f: F,
787 ) -> std::result::Result<T, E>
788 where
789 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
790 E: From<std::io::Error>,
791 {
792 let destname = destname.as_ref();
793 let (d, name) = subdir_of(self, destname)?;
794 let existing_metadata = d.symlink_metadata_optional(destname)?;
795 let existing_perms = existing_metadata
798 .filter(|m| m.is_file())
799 .map(|m| m.permissions());
800 let mut t = cap_tempfile::TempFile::new(&d)?;
801 if let Some(existing_perms) = existing_perms {
803 t.as_file_mut().set_permissions(existing_perms)?;
804 }
805 let mut bufw = std::io::BufWriter::new(t);
807 let r = f(&mut bufw)?;
809 t = bufw.into_inner().map_err(From::from)?;
811 t.as_file().sync_all()?;
813 t.replace(name)?;
815 d.open(".")?.sync_all()?;
817 Ok(r)
818 }
819
820 #[cfg(not(windows))]
821 fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
822 self.atomic_replace_with(destname, |f| f.write_all(contents.as_ref()))
823 }
824
825 #[cfg(not(windows))]
826 fn atomic_write_with_perms(
827 &self,
828 destname: impl AsRef<Path>,
829 contents: impl AsRef<[u8]>,
830 perms: cap_std::fs::Permissions,
831 ) -> Result<()> {
832 self.atomic_replace_with(destname, |f| -> io::Result<_> {
833 #[cfg(unix)]
837 {
838 use cap_std::fs::PermissionsExt;
839 let perms = cap_std::fs::Permissions::from_mode(0o600);
840 f.get_mut().as_file_mut().set_permissions(perms)?;
841 }
842 f.write_all(contents.as_ref())?;
843 f.flush()?;
844 f.get_mut().as_file_mut().set_permissions(perms)?;
845 Ok(())
846 })
847 }
848
849 #[cfg(unix)]
850 fn reopen_as_ownedfd(&self) -> Result<OwnedFd> {
851 use rustix::fs::{Mode, OFlags};
852 use std::os::fd::AsFd;
853 rustix::fs::openat(
854 self.as_fd(),
855 ".",
856 OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::RDONLY,
857 Mode::empty(),
858 )
859 .map_err(Into::into)
860 }
861
862 #[cfg(any(target_os = "android", target_os = "linux"))]
863 fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>> {
864 is_mountpoint_impl_statx(self, path.as_ref())
865 }
866
867 #[cfg(any(target_os = "android", target_os = "linux"))]
868 fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>> {
869 crate::xattrs::impl_getxattr(self, path.as_ref(), key.as_ref())
870 }
871
872 #[cfg(any(target_os = "android", target_os = "linux"))]
873 fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList> {
874 crate::xattrs::impl_listxattrs(self, path.as_ref())
875 }
876
877 #[cfg(any(target_os = "android", target_os = "linux"))]
878 fn setxattr(
879 &self,
880 path: impl AsRef<Path>,
881 key: impl AsRef<OsStr>,
882 value: impl AsRef<[u8]>,
883 ) -> Result<()> {
884 crate::xattrs::impl_setxattr(self, path.as_ref(), key.as_ref(), value.as_ref())
885 }
886
887 fn walk<C, E>(&self, config: &WalkConfiguration, mut callback: C) -> std::result::Result<(), E>
888 where
889 C: FnMut(&WalkComponent) -> WalkResult<E>,
890 E: From<std::io::Error>,
891 {
892 let mut pb = config
893 .path_base
894 .as_ref()
895 .map(|v| v.to_path_buf())
896 .unwrap_or_default();
897 walk_inner(self, &mut pb, &mut callback, config)
898 }
899}
900
901#[cfg(feature = "fs_utf8")]
905impl CapStdExtDirExtUtf8 for cap_std::fs_utf8::Dir {
906 fn open_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::File>> {
907 map_optional(self.open(path.as_ref()))
908 }
909
910 fn open_dir_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::Dir>> {
911 map_optional(self.open_dir(path.as_ref()))
912 }
913
914 fn ensure_dir_with(
915 &self,
916 p: impl AsRef<Utf8Path>,
917 builder: &cap_std::fs::DirBuilder,
918 ) -> Result<bool> {
919 self.as_cap_std()
920 .ensure_dir_with(p.as_ref().as_std_path(), builder)
921 }
922
923 fn metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>> {
924 self.as_cap_std()
925 .metadata_optional(path.as_ref().as_std_path())
926 }
927
928 fn symlink_metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>> {
929 self.as_cap_std()
930 .symlink_metadata_optional(path.as_ref().as_std_path())
931 }
932
933 fn remove_file_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool> {
934 self.as_cap_std()
935 .remove_file_optional(path.as_ref().as_std_path())
936 }
937
938 fn remove_all_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool> {
939 self.as_cap_std()
940 .remove_all_optional(path.as_ref().as_std_path())
941 }
942
943 #[cfg(unix)]
944 fn update_timestamps(&self, path: impl AsRef<Utf8Path>) -> Result<()> {
945 self.as_cap_std()
946 .update_timestamps(path.as_ref().as_std_path())
947 }
948
949 fn atomic_replace_with<F, T, E>(
950 &self,
951 destname: impl AsRef<Utf8Path>,
952 f: F,
953 ) -> std::result::Result<T, E>
954 where
955 F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
956 E: From<std::io::Error>,
957 {
958 self.as_cap_std()
959 .atomic_replace_with(destname.as_ref().as_std_path(), f)
960 }
961
962 fn atomic_write(
963 &self,
964 destname: impl AsRef<Utf8Path>,
965 contents: impl AsRef<[u8]>,
966 ) -> Result<()> {
967 self.as_cap_std()
968 .atomic_write(destname.as_ref().as_std_path(), contents)
969 }
970
971 fn atomic_write_with_perms(
972 &self,
973 destname: impl AsRef<Utf8Path>,
974 contents: impl AsRef<[u8]>,
975 perms: cap_std::fs::Permissions,
976 ) -> Result<()> {
977 self.as_cap_std()
978 .atomic_write_with_perms(destname.as_ref().as_std_path(), contents, perms)
979 }
980
981 fn filenames_filtered_sorted_by<F, C>(&self, mut f: F, mut compare: C) -> Result<Vec<String>>
982 where
983 F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
984 C: FnMut(&str, &str) -> std::cmp::Ordering,
985 {
986 let mut r =
987 self.entries()?
988 .try_fold(Vec::new(), |mut acc, ent| -> Result<Vec<String>> {
989 let ent = ent?;
990 let name = ent.file_name()?;
991 if f(&ent, name.as_str()) {
992 acc.push(name);
993 }
994 Ok(acc)
995 })?;
996 r.sort_by(|a, b| compare(a.as_str(), b.as_str()));
997 Ok(r)
998 }
999}
1000
1001#[cfg(test)]
1002mod tests {
1003 use std::path::Path;
1004
1005 use super::*;
1006
1007 #[test]
1008 fn test_validate_relpath_no_uplinks() {
1009 let ok_cases = ["foo", "foo/bar", "foo/bar/"];
1010 #[cfg(unix)]
1011 let err_cases = ["/foo", "/", "../foo", "foo/../bar"];
1012 #[cfg(windows)]
1013 let err_cases = ["C:\\foo", "../foo", "foo/../bar"];
1014
1015 for case in ok_cases {
1016 assert!(validate_relpath_no_uplinks(Path::new(case)).is_ok());
1017 }
1018 for case in err_cases {
1019 assert!(validate_relpath_no_uplinks(Path::new(case)).is_err());
1020 }
1021 }
1022}