1use std::ffi::{OsStr, OsString};
11use std::fmt;
12use std::io;
13use std::path::{Component, Path, PathBuf};
14use std::sync::Arc;
15
16use cap_fs_ext::{DirExt, OpenOptionsSyncExt};
17use cap_std::fs::{Dir, File, Metadata, OpenOptions, ReadDir};
18
19use crate::runtime::Vfs;
20use crate::runtime::vfs::{Access, Mount, Placement};
21
22pub(crate) mod removal;
23
24#[derive(Debug, PartialEq, Eq, Clone)]
25pub enum ResolveError {
26 Escape,
27 EmbeddedNul,
28 UnsupportedPrefix,
29}
30
31impl fmt::Display for ResolveError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::Escape => f.write_str("path escapes the VFS root"),
35 Self::EmbeddedNul => f.write_str("path contains a NUL byte"),
36 Self::UnsupportedPrefix => f.write_str("Windows-style path prefixes are not supported"),
37 }
38 }
39}
40
41impl std::error::Error for ResolveError {}
42
43fn push_component(stack: &mut Vec<OsString>, comp: Component<'_>) -> Result<(), ResolveError> {
44 match comp {
45 Component::Prefix(_) => Err(ResolveError::UnsupportedPrefix),
46 Component::RootDir | Component::CurDir => Ok(()),
48 Component::ParentDir => {
49 if stack.pop().is_none() {
50 Err(ResolveError::Escape)
51 } else {
52 Ok(())
53 }
54 }
55 Component::Normal(name) => {
56 stack.push(name.to_os_string());
57 Ok(())
58 }
59 }
60}
61
62#[derive(Debug)]
69pub enum ContainError {
70 Escape,
72 EmbeddedNul,
73 UnsupportedPrefix,
74 Disabled,
76 ReadOnly(Arc<str>),
78 MountPoint(Arc<str>),
81 Io(io::Error),
83}
84
85impl fmt::Display for ContainError {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 Self::Escape => f.write_str("path escapes the VFS root"),
89 Self::EmbeddedNul => f.write_str("path contains a NUL byte"),
90 Self::UnsupportedPrefix => f.write_str("Windows-style path prefixes are not supported"),
91 Self::Disabled => f.write_str("filesystem is disabled (vfs mode: none)"),
92 Self::ReadOnly(mount) => write!(f, "the volume mounted at {mount} is read-only"),
93 Self::MountPoint(mount) => write!(
94 f,
95 "{mount} is a mount point; mount points and the directories above them \
96 cannot be removed, moved, replaced or written through"
97 ),
98 Self::Io(e) => write!(f, "{e}"),
99 }
100 }
101}
102
103impl std::error::Error for ContainError {}
104
105impl From<ResolveError> for ContainError {
106 fn from(err: ResolveError) -> Self {
107 match err {
108 ResolveError::Escape => Self::Escape,
109 ResolveError::EmbeddedNul => Self::EmbeddedNul,
110 ResolveError::UnsupportedPrefix => Self::UnsupportedPrefix,
111 }
112 }
113}
114
115impl From<io::Error> for ContainError {
116 fn from(err: io::Error) -> Self {
117 if is_escape(&err) {
118 Self::Escape
119 } else {
120 Self::Io(err)
121 }
122 }
123}
124
125pub fn is_escape(err: &io::Error) -> bool {
132 err.kind() == io::ErrorKind::PermissionDenied && err.raw_os_error().is_none()
133}
134
135#[derive(Clone)]
147pub struct ContentPath {
148 dir: Arc<Dir>,
149 rel: PathBuf,
150 placement: Placement,
151}
152
153impl fmt::Debug for ContentPath {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.debug_struct("ContentPath")
156 .field("rel", &self.rel)
157 .finish_non_exhaustive()
158 }
159}
160
161impl ContentPath {
162 pub(crate) fn new(dir: Arc<Dir>, rel: PathBuf, placement: Placement) -> Self {
163 Self {
164 dir,
165 rel,
166 placement,
167 }
168 }
169
170 pub fn placement(&self) -> &Placement {
172 &self.placement
173 }
174
175 pub fn mounts_below(&self) -> Vec<(PathBuf, Arc<Dir>)> {
179 let base = if self.rel == Path::new(".") {
180 Path::new("")
181 } else {
182 self.rel.as_path()
183 };
184 self.placement
185 .nested()
186 .iter()
187 .filter_map(|mount| {
188 let rest = mount.rel().strip_prefix(base).ok()?;
189 (!rest.as_os_str().is_empty())
190 .then(|| (rest.to_path_buf(), Arc::clone(mount.dir())))
191 })
192 .collect()
193 }
194
195 pub fn writable(&self) -> Result<(), ContainError> {
199 match self.placement.access() {
200 Access::ReadWrite => Ok(()),
201 Access::ReadOnly => Err(ContainError::ReadOnly(Arc::from(
202 self.placement.mount_point(),
203 ))),
204 }
205 }
206
207 fn check_mutation(&self, recursive: bool) -> Result<(), ContainError> {
208 self.writable()?;
209 check_mount_points(&self.dir, &self.rel, &self.placement, recursive, true)?;
210 check_metadata_mutation(&self.dir, &self.rel, recursive, true)
211 }
212
213 fn check_link_mutation(&self, recursive: bool) -> Result<(), ContainError> {
214 self.writable()?;
215 check_mount_points(&self.dir, &self.rel, &self.placement, recursive, false)?;
216 check_metadata_mutation(&self.dir, &self.rel, recursive, false)
217 }
218
219 pub fn open(&self) -> Result<File, ContainError> {
220 Ok(self.dir.open(&self.rel)?)
221 }
222
223 pub(crate) fn open_regular(&self) -> Result<(File, Metadata), ContainError> {
226 require_regular_file(&self.metadata()?)?;
227 let mut options = OpenOptions::new();
228 options.read(true).nonblock(true);
229 let file = self.dir.open_with(&self.rel, &options)?;
230 let metadata = file.metadata()?;
231 require_regular_file(&metadata)?;
232 Ok((file, metadata))
233 }
234
235 pub fn create(&self) -> Result<File, ContainError> {
236 self.check_mutation(false)?;
237 Ok(self.dir.create(&self.rel)?)
238 }
239
240 pub fn create_new(&self) -> Result<File, ContainError> {
243 self.check_mutation(false)?;
244 let mut opts = OpenOptions::new();
245 opts.write(true).create_new(true);
246 Ok(self.dir.open_with(&self.rel, &opts)?)
247 }
248
249 pub fn append(&self) -> Result<File, ContainError> {
250 self.check_mutation(false)?;
251 let mut opts = OpenOptions::new();
252 opts.create(true).append(true);
253 Ok(self.dir.open_with(&self.rel, &opts)?)
254 }
255
256 pub fn read(&self) -> Result<Vec<u8>, ContainError> {
257 Ok(self.dir.read(&self.rel)?)
258 }
259
260 pub fn metadata(&self) -> Result<Metadata, ContainError> {
262 Ok(self.dir.metadata(&self.rel)?)
263 }
264
265 pub fn regular_file(&self) -> Option<(FileIdentity, u64)> {
269 regular_file(self.dir.symlink_metadata(&self.rel).ok()?)
270 }
271
272 pub fn file_len(&self) -> u64 {
274 self.regular_file().map_or(0, |(_, bytes)| bytes)
275 }
276
277 pub fn refers_to(&self, identity: FileIdentity) -> Result<bool, ContainError> {
281 match self.dir.symlink_metadata(&self.rel) {
282 Ok(metadata) => Ok(regular_file(metadata).is_some_and(|(file, _)| file == identity)),
283 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
284 Err(e) => Err(e.into()),
285 }
286 }
287
288 pub fn try_exists(&self) -> Result<bool, ContainError> {
292 match self.dir.metadata(&self.rel) {
293 Ok(_) => Ok(true),
294 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
295 Err(e) => Err(e.into()),
296 }
297 }
298
299 pub fn create_dir(&self) -> Result<(), ContainError> {
300 self.check_mutation(false)?;
301 Ok(self.dir.create_dir(&self.rel)?)
302 }
303
304 pub fn is_existing_dir(&self) -> bool {
307 self.dir.metadata(&self.rel).is_ok_and(|meta| meta.is_dir())
308 }
309
310 pub fn create_dir_all(&self) -> Result<(), ContainError> {
313 if self.is_existing_dir() {
314 return Ok(());
315 }
316 self.check_mutation(false)?;
317 Ok(self.dir.create_dir_all(&self.rel)?)
318 }
319
320 pub fn open_dir(&self) -> Result<Dir, ContainError> {
321 Ok(self.dir.open_dir(&self.rel)?)
322 }
323
324 pub fn entries(&self) -> Result<ReadDir, ContainError> {
325 Ok(self.dir.read_dir(&self.rel)?)
326 }
327
328 pub fn remove_file(&self) -> Result<(), ContainError> {
329 self.check_mutation(false)?;
330 Ok(self.dir.remove_file(&self.rel)?)
331 }
332
333 pub fn rename_to(&self, dest: &Self) -> Result<(), ContainError> {
336 self.check_rename_end()?;
337 dest.check_rename_end()?;
338 Ok(self.dir.rename(&self.rel, &dest.dir, &dest.rel)?)
339 }
340
341 pub fn check_rename_end(&self) -> Result<(), ContainError> {
345 self.writable()?;
346 check_mount_points(&self.dir, &self.rel, &self.placement, true, false)?;
347 check_metadata_mutation(&self.dir, &self.rel, true, true)
348 }
349
350 pub fn is_root(&self) -> bool {
355 self.rel == Path::new(".")
356 }
357
358 #[cfg(unix)]
360 pub(crate) fn sync_parent(&self) -> Result<(), ContainError> {
361 let parent = self
362 .rel
363 .parent()
364 .filter(|p| !p.as_os_str().is_empty())
365 .unwrap_or(Path::new("."));
366 self.dir.open_dir(parent)?.open(".")?.sync_all()?;
367 Ok(())
368 }
369
370 pub fn temp_sibling(&self) -> Self {
373 let mut name = self
374 .rel
375 .file_name()
376 .map_or_else(|| OsString::from("file"), OsStr::to_os_string);
377 name.push(temp_suffix());
378 let mut rel = self.rel.parent().map(Path::to_path_buf).unwrap_or_default();
379 rel.push(name);
380 Self {
381 dir: Arc::clone(&self.dir),
382 rel,
383 placement: self.placement.clone(),
384 }
385 }
386}
387
388#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
392pub struct FileIdentity {
393 dev: u64,
394 ino: u64,
395}
396
397impl FileIdentity {
398 pub fn of(metadata: &Metadata) -> io::Result<Self> {
401 #[cfg(not(windows))]
402 {
403 Ok(Self {
404 dev: cap_fs_ext::MetadataExt::dev(metadata),
405 ino: cap_fs_ext::MetadataExt::ino(metadata),
406 })
407 }
408 #[cfg(windows)]
409 {
410 use cap_primitives::fs::_WindowsByHandle;
411 match (metadata.volume_serial_number(), metadata.file_index()) {
412 (Some(dev), Some(ino)) => Ok(Self {
413 dev: dev.into(),
414 ino,
415 }),
416 _ => Err(io::Error::other(
417 "a file's identity needs metadata read through an open handle",
418 )),
419 }
420 }
421 }
422}
423
424pub fn link_count(metadata: &Metadata) -> Option<u64> {
427 #[cfg(not(windows))]
428 {
429 Some(cap_fs_ext::MetadataExt::nlink(metadata))
430 }
431 #[cfg(windows)]
432 {
433 use cap_primitives::fs::_WindowsByHandle;
434 metadata.number_of_links().map(u64::from)
435 }
436}
437
438fn regular_file(metadata: Metadata) -> Option<(FileIdentity, u64)> {
441 if !metadata.is_file() {
442 return None;
443 }
444 let identity = FileIdentity::of(&metadata).ok()?;
445 Some((identity, metadata.len()))
446}
447
448fn require_regular_file(metadata: &Metadata) -> Result<(), ContainError> {
449 if metadata.is_file() {
450 return Ok(());
451 }
452 Err(io::Error::new(io::ErrorKind::InvalidInput, "not a regular file").into())
453}
454
455#[derive(Clone)]
465pub struct LinkPath {
466 parent: Arc<Dir>,
467 name: OsString,
468 guard: ContentPath,
469}
470
471impl fmt::Debug for LinkPath {
472 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473 f.debug_struct("LinkPath")
474 .field("name", &self.name)
475 .finish_non_exhaustive()
476 }
477}
478
479#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub enum LinkKind {
483 File,
484 Directory,
485}
486
487impl LinkPath {
488 pub(crate) fn child(&self, parent: Arc<Dir>, name: OsString) -> Self {
489 let guard = ContentPath::new(
490 Arc::clone(&self.guard.dir),
491 self.guard.rel.join(&name),
492 self.guard.placement.clone(),
493 );
494 Self {
495 parent,
496 name,
497 guard,
498 }
499 }
500
501 pub fn is_root(&self) -> bool {
504 self.name == OsStr::new(".")
505 }
506
507 pub fn placement(&self) -> &Placement {
509 &self.guard.placement
510 }
511
512 pub fn writable(&self) -> Result<(), ContainError> {
514 self.guard.writable()
515 }
516
517 pub fn contains_mount_point(&self) -> Result<Option<Arc<str>>, ContainError> {
521 let placement = &self.guard.placement;
522 if placement.nested().is_empty() {
523 return Ok(None);
524 }
525 if let Some(mount) = mount_below(&self.guard.rel, placement) {
526 return Ok(Some(mount));
527 }
528 let real = real_location(&self.guard.dir, &self.guard.rel, false, placement)?;
529 Ok(mount_below(&real, placement))
530 }
531
532 pub fn same_entry(&self, other: &Self) -> Result<bool, ContainError> {
534 let source = self.symlink_metadata()?;
535 let target = match other.symlink_metadata() {
536 Ok(meta) => meta,
537 Err(ContainError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
538 return Ok(false);
539 }
540 Err(error) => return Err(error),
541 };
542 Ok(FileIdentity::of(&source)? == FileIdentity::of(&target)?)
543 }
544
545 pub fn check_copy_destination(&self, to: &ContentPath) -> Result<(), ContainError> {
547 let source = self.symlink_metadata()?;
548 let identity = FileIdentity::of(&source)?;
549 let invalid = || {
550 ContainError::Io(io::Error::new(
551 io::ErrorKind::InvalidInput,
552 "cannot copy an entry onto itself or a directory into its own tree",
553 ))
554 };
555 if source.is_dir() && to.placement.ancestors().contains(&identity) {
556 return Err(invalid());
557 }
558 let mut prefix = PathBuf::from(".");
559 let parts = std::iter::once(None).chain(to.rel.components().map(Some));
560 for part in parts {
561 if let Some(part) = part {
562 prefix.push(part.as_os_str());
563 }
564 match to.dir.metadata(&prefix) {
565 Ok(meta) => {
566 if FileIdentity::of(&meta)? == identity
567 && (source.is_dir()
568 || prefix == to.rel
569 || prefix.strip_prefix(".").is_ok_and(|p| p == to.rel))
570 {
571 return Err(invalid());
572 }
573 }
574 Err(error)
575 if matches!(
576 error.kind(),
577 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
578 ) =>
579 {
580 break;
581 }
582 Err(error) => return Err(error.into()),
583 }
584 }
585 Ok(())
586 }
587
588 pub fn temp_sibling(&self) -> Self {
591 let mut name = self.name.clone();
592 name.push(temp_suffix());
593 let rel = self
594 .guard
595 .rel
596 .parent()
597 .map_or_else(|| PathBuf::from(&name), |parent| parent.join(&name));
598 Self {
599 parent: Arc::clone(&self.parent),
600 name,
601 guard: ContentPath::new(
602 Arc::clone(&self.guard.dir),
603 rel,
604 self.guard.placement.clone(),
605 ),
606 }
607 }
608
609 pub fn symlink_metadata(&self) -> Result<Metadata, ContainError> {
610 Ok(self.parent.symlink_metadata(&self.name)?)
611 }
612
613 pub fn read_link_contents(&self) -> Result<PathBuf, ContainError> {
614 Ok(self.parent.read_link_contents(&self.name)?)
615 }
616
617 pub fn link_kind(&self) -> Result<LinkKind, ContainError> {
622 #[cfg(not(windows))]
623 {
624 Ok(LinkKind::File)
625 }
626 #[cfg(windows)]
627 {
628 use cap_std::fs::MetadataExt;
629
630 const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
631 let attrs = self.symlink_metadata()?.file_attributes();
632 Ok(if attrs & FILE_ATTRIBUTE_DIRECTORY == 0 {
633 LinkKind::File
634 } else {
635 LinkKind::Directory
636 })
637 }
638 }
639
640 pub fn symlink(&self, target: &Path, kind: LinkKind) -> Result<(), ContainError> {
655 self.guard.check_link_mutation(false)?;
656 #[cfg(not(windows))]
657 {
658 let _ = kind;
659 Ok(self.parent.symlink_contents(target, &self.name)?)
660 }
661 #[cfg(windows)]
662 {
663 match kind {
664 LinkKind::Directory => Ok(DirExt::symlink_dir(&*self.parent, target, &self.name)?),
665 LinkKind::File => Ok(DirExt::symlink_file(&*self.parent, target, &self.name)?),
666 }
667 }
668 }
669
670 pub fn remove_file(&self) -> Result<(), ContainError> {
671 self.guard.check_link_mutation(false)?;
672 Ok(self.parent.remove_file_or_symlink(&self.name)?)
675 }
676
677 pub fn remove_dir(&self) -> Result<(), ContainError> {
678 self.guard.check_link_mutation(true)?;
679 Ok(self.parent.remove_dir(&self.name)?)
680 }
681
682 pub fn remove_dir_all(&self) -> Result<(), ContainError> {
683 self.check_removable()?;
684 Ok(self.parent.remove_dir_all(&self.name)?)
685 }
686
687 pub fn check_removable(&self) -> Result<(), ContainError> {
691 self.guard.check_link_mutation(true)
692 }
693
694 pub fn create_dir_all(&self) -> Result<(), ContainError> {
695 if self
696 .parent
697 .metadata(&self.name)
698 .is_ok_and(|meta| meta.is_dir())
699 {
700 return Ok(());
701 }
702 self.guard.check_link_mutation(false)?;
703 match self.parent.create_dir(&self.name) {
704 Ok(()) => Ok(()),
705 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
706 Err(e) => Err(e.into()),
707 }
708 }
709
710 pub fn open_dir(&self) -> Result<Dir, ContainError> {
713 Ok(self.parent.open_dir(&self.name)?)
714 }
715
716 pub fn regular_file(&self) -> Option<(FileIdentity, u64)> {
719 regular_file(self.symlink_metadata().ok()?)
720 }
721
722 pub fn copied_onto_file(&self) -> Option<(FileIdentity, u64)> {
725 regular_file(self.parent.metadata(&self.name).ok()?)
726 }
727
728 pub fn entries(&self) -> Result<ReadDir, ContainError> {
729 Ok(self.parent.read_dir(&self.name)?)
730 }
731
732 pub fn copy_to(&self, dest: &Self) -> Result<(), ContainError> {
736 self.copy_to_counted(dest).map(|_| ())
737 }
738
739 pub(crate) fn copy_to_counted(&self, dest: &Self) -> Result<u64, ContainError> {
740 dest.guard.check_mutation(true)?;
741 Ok(self.parent.copy(&self.name, &dest.parent, &dest.name)?)
742 }
743
744 pub fn rename_to(&self, dest: &Self) -> Result<(), ContainError> {
747 self.check_removable()?;
748 dest.check_removable()?;
749 Ok(self.parent.rename(&self.name, &dest.parent, &dest.name)?)
750 }
751}
752
753fn temp_suffix() -> String {
758 static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
759 let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
760 let pid = std::process::id();
761 let nanos = std::time::SystemTime::now()
762 .duration_since(std::time::UNIX_EPOCH)
763 .map_or(0, |d| d.as_nanos());
764 format!(".{nanos}.{pid}.{seq}.tmp")
765}
766
767pub fn resolve_content(
772 vfs: &Vfs,
773 cwd: &str,
774 guest_path: &str,
775) -> Result<ContentPath, ContainError> {
776 vfs.dir().ok_or(ContainError::Disabled)?;
778 let rel = relative(cwd, guest_path)?;
779 let (dir, rel, placement) = vfs.locate(&rel).ok_or(ContainError::Disabled)?;
780 Ok(ContentPath::new(dir, rel, placement))
781}
782
783pub fn resolve_link(vfs: &Vfs, cwd: &str, guest_path: &str) -> Result<LinkPath, ContainError> {
792 vfs.dir().ok_or(ContainError::Disabled)?;
794 let rel = relative(cwd, guest_path)?;
795 let (root, rel, placement) = vfs.locate(&rel).ok_or(ContainError::Disabled)?;
796 let Some(name) = rel.file_name() else {
797 let name = OsString::from(".");
798 let guard = ContentPath::new(Arc::clone(&root), PathBuf::from(&name), placement);
799 return Ok(LinkPath {
800 parent: root,
801 name,
802 guard,
803 });
804 };
805 let name = name.to_os_string();
806 let parent_rel = rel.parent().unwrap_or(Path::new(""));
807 let parent = if parent_rel.as_os_str().is_empty() {
808 Arc::clone(&root)
809 } else {
810 Arc::new(root.open_dir(parent_rel)?)
811 };
812 Ok(LinkPath {
813 parent,
814 name,
815 guard: ContentPath::new(root, rel, placement),
816 })
817}
818
819pub fn guest_normalize(cwd: &str, guest_path: &str) -> Result<String, ResolveError> {
824 let rel = relative(cwd, guest_path)?;
825 if rel == Path::new(".") {
826 return Ok(String::from("/"));
827 }
828 let components = rel
830 .components()
831 .map(|component| component.as_os_str().to_string_lossy())
832 .collect::<Vec<_>>();
833 Ok(format!("/{}", components.join("/")))
834}
835
836fn relative(cwd: &str, guest_path: &str) -> Result<PathBuf, ResolveError> {
840 if guest_path.as_bytes().contains(&0) || cwd.as_bytes().contains(&0) {
841 return Err(ResolveError::EmbeddedNul);
842 }
843 let path = Path::new(guest_path);
844 let is_absolute = matches!(path.components().next(), Some(Component::RootDir));
845
846 let mut stack: Vec<OsString> = Vec::new();
847 if !is_absolute {
848 for comp in Path::new(cwd).components() {
849 push_component(&mut stack, comp)?;
850 }
851 }
852 for comp in path.components() {
853 push_component(&mut stack, comp)?;
854 }
855
856 if stack.is_empty() {
857 return Ok(PathBuf::from("."));
858 }
859 let mut out = PathBuf::new();
860 for name in &stack {
861 out.push(name);
862 }
863 Ok(out)
864}
865
866pub fn protected_metadata(path: &Path) -> bool {
867 path.components().any(|part| {
868 let name = part.as_os_str().to_string_lossy();
869 let options = gix::validate::path::component::Options {
870 protect_windows: false,
871 ..Default::default()
872 };
873 let reserved_alias = matches!(
874 gix::validate::path::component(name.as_bytes().into(), None, options),
875 Err(gix::validate::path::component::Error::DotGitDir)
876 );
877 reserved_alias
878 || name
879 .trim_end_matches([' ', '.'])
880 .eq_ignore_ascii_case(".git")
881 || name.to_ascii_lowercase().starts_with(".git-submilli-")
882 })
883}
884
885pub(crate) fn check_repository_clear_of_mounts(
891 root: &Dir,
892 path: &Path,
893 placement: &Placement,
894) -> Result<(), ContainError> {
895 check_nested_mount_points(root, path, placement, true, true)?;
896 match root.metadata(path) {
897 Ok(meta) if meta.is_dir() => {
898 if placement.protects_descendants(dir_identity(root, path, &meta)?) {
899 return Err(ContainError::MountPoint(Arc::from(placement.mount_point())));
900 }
901 }
902 Ok(_) => {}
903 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
904 Err(error) => return Err(error.into()),
905 }
906 Ok(())
907}
908
909pub fn check_mount_points(
918 root: &Dir,
919 path: &Path,
920 placement: &Placement,
921 recursive: bool,
922 follow_final: bool,
923) -> Result<(), ContainError> {
924 check_nested_mount_points(root, path, placement, recursive, follow_final)?;
925 let metadata = if follow_final {
926 root.metadata(path)
927 } else {
928 root.symlink_metadata(path)
929 };
930 match metadata {
931 Ok(meta) if meta.is_dir() => {
932 if placement.protects(dir_identity(root, path, &meta)?, recursive) {
933 return Err(ContainError::MountPoint(Arc::from(placement.mount_point())));
934 }
935 }
936 Ok(_) => {}
937 Err(error)
938 if matches!(
939 error.kind(),
940 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
941 ) => {}
942 Err(error) => return Err(error.into()),
943 }
944 Ok(())
945}
946
947fn check_nested_mount_points(
948 root: &Dir,
949 path: &Path,
950 placement: &Placement,
951 recursive: bool,
952 follow_final: bool,
953) -> Result<(), ContainError> {
954 if placement.nested().is_empty() {
955 return Ok(());
956 }
957 if let Some(mount) = mount_at_or_below(path, placement, recursive) {
958 return Err(ContainError::MountPoint(mount));
959 }
960 let real = real_location(root, path, follow_final, placement)?;
961 if let Some(mount) = mount_at_or_below(&real, placement, recursive) {
962 return Err(ContainError::MountPoint(mount));
963 }
964 Ok(())
965}
966
967fn real_location(
979 root: &Dir,
980 path: &Path,
981 follow_final: bool,
982 placement: &Placement,
983) -> Result<PathBuf, ContainError> {
984 let mut pending: Vec<OsString> = Vec::new();
985 push_reversed(&mut pending, path);
986 let mut resolved = PathBuf::new();
987 let mut missing = false;
988 let mut hops = 0usize;
989 while let Some(name) = pending.pop() {
990 if name == OsStr::new(".") {
991 continue;
992 }
993 if name == OsStr::new("..") {
994 if !resolved.pop() {
995 return Err(ContainError::Escape);
996 }
997 continue;
998 }
999 let candidate = resolved.join(&name);
1000 if missing {
1001 resolved = candidate;
1002 continue;
1003 }
1004 let follow_this = follow_final || !pending.is_empty();
1005 match root.symlink_metadata(&candidate) {
1006 Ok(meta) if meta.file_type().is_symlink() && follow_this => {
1007 hops += 1;
1008 if hops > MAX_LINK_HOPS {
1009 return Err(ContainError::Io(io::Error::other(
1010 "too many levels of symbolic links",
1011 )));
1012 }
1013 let target = root.read_link_contents(&candidate)?;
1014 if target.has_root() {
1015 return Err(ContainError::Escape);
1016 }
1017 push_reversed(&mut pending, &target);
1018 }
1019 Ok(meta) => match placeholder_name(placement, root, &candidate, &resolved, &meta) {
1020 Ok(Some(exact)) => resolved.push(exact),
1021 Ok(None) => resolved = candidate,
1022 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1024 missing = true;
1025 resolved = candidate;
1026 }
1027 Err(error) => return Err(error.into()),
1028 },
1029 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1030 missing = true;
1031 resolved = candidate;
1032 }
1033 Err(error) => return Err(error.into()),
1034 }
1035 }
1036 Ok(resolved)
1037}
1038
1039fn placeholder_name<'a>(
1047 placement: &'a Placement,
1048 root: &Dir,
1049 path: &Path,
1050 parent: &Path,
1051 meta: &Metadata,
1052) -> io::Result<Option<&'a OsStr>> {
1053 if !meta.is_dir() {
1054 return Ok(None);
1055 }
1056 let mut beside = placement
1057 .nested()
1058 .iter()
1059 .flat_map(Mount::placeholders)
1060 .filter(|placeholder| placeholder.parent == parent)
1061 .peekable();
1062 if beside.peek().is_none() {
1063 return Ok(None);
1064 }
1065 let identity = dir_identity(root, path, meta)?;
1066 Ok(beside
1067 .find(|placeholder| placeholder.identity == identity)
1068 .map(|placeholder| placeholder.name.as_os_str()))
1069}
1070
1071pub(crate) fn dir_identity(root: &Dir, path: &Path, meta: &Metadata) -> io::Result<FileIdentity> {
1075 match FileIdentity::of(meta) {
1076 Ok(identity) => Ok(identity),
1077 Err(_) => FileIdentity::of(&root.open_dir(path)?.dir_metadata()?),
1078 }
1079}
1080
1081fn push_reversed(pending: &mut Vec<OsString>, path: &Path) {
1083 pending.extend(
1084 path.components()
1085 .rev()
1086 .map(|part| part.as_os_str().to_os_string()),
1087 );
1088}
1089
1090const MAX_LINK_HOPS: usize = 40;
1092
1093fn mount_at_or_below(path: &Path, placement: &Placement, recursive: bool) -> Option<Arc<str>> {
1095 placement.nested().iter().find_map(|mount| {
1096 let inside = starts_with_component(path, mount.rel());
1097 let above = recursive && starts_with_component(mount.rel(), path);
1098 (inside || above).then(|| Arc::from(mount.guest_path()))
1099 })
1100}
1101
1102fn mount_below(path: &Path, placement: &Placement) -> Option<Arc<str>> {
1104 placement.nested().iter().find_map(|mount| {
1105 let below = starts_with_component(mount.rel(), path) && mount.rel() != path;
1106 below.then(|| Arc::from(mount.guest_path()))
1107 })
1108}
1109
1110fn starts_with_component(path: &Path, prefix: &Path) -> bool {
1114 if prefix == Path::new(".") {
1115 return true;
1116 }
1117 let mut parts = path.components().filter(|c| *c != Component::CurDir);
1118 for want in prefix.components().filter(|c| *c != Component::CurDir) {
1119 match parts.next() {
1120 Some(have) if same_component(have.as_os_str(), want.as_os_str()) => {}
1121 _ => return false,
1122 }
1123 }
1124 true
1125}
1126
1127fn same_component(a: &OsStr, b: &OsStr) -> bool {
1128 if cfg!(any(target_os = "macos", windows)) {
1129 a.to_string_lossy()
1130 .eq_ignore_ascii_case(&b.to_string_lossy())
1131 } else {
1132 a == b
1133 }
1134}
1135
1136fn metadata_denied() -> ContainError {
1137 ContainError::Io(io::Error::other(
1138 "Git metadata is protected; use submilli:git with Git capabilities",
1139 ))
1140}
1141
1142fn check_metadata_mutation(
1143 root: &Dir,
1144 path: &Path,
1145 recursive: bool,
1146 follow_final: bool,
1147) -> Result<(), ContainError> {
1148 if protected_metadata(path) {
1149 return Err(metadata_denied());
1150 }
1151 match check_metadata_prefixes(root, path, follow_final, &mut |_| {
1152 Ok::<_, std::convert::Infallible>(())
1153 }) {
1154 Ok(()) => {}
1155 Err(PrefixError::Filesystem(error)) => return Err(error),
1156 Err(PrefixError::Work(never)) => match never {},
1157 }
1158 match root.symlink_metadata(path) {
1159 Ok(meta) => {
1160 #[cfg(unix)]
1161 {
1162 use cap_std::fs::MetadataExt;
1163 if meta.is_file() && meta.nlink() > 1 {
1164 return Err(metadata_denied());
1165 }
1166 }
1167 if recursive && meta.is_dir() {
1168 reject_metadata_descendants(&root.open_dir(path)?, &mut 0, 0)?;
1169 }
1170 }
1171 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
1172 Err(error) => return Err(error.into()),
1173 }
1174 Ok(())
1175}
1176
1177#[derive(Debug)]
1178enum PrefixError<E> {
1179 Filesystem(ContainError),
1180 Work(E),
1181}
1182
1183impl<E> From<io::Error> for PrefixError<E> {
1184 fn from(error: io::Error) -> Self {
1185 Self::Filesystem(error.into())
1186 }
1187}
1188
1189fn check_metadata_prefixes<E>(
1190 root: &Dir,
1191 path: &Path,
1192 follow_final: bool,
1193 work: &mut impl FnMut(u64) -> Result<(), E>,
1194) -> Result<(), PrefixError<E>> {
1195 if protected_metadata(path) {
1196 return Err(PrefixError::Filesystem(metadata_denied()));
1197 }
1198 work(1).map_err(PrefixError::Work)?;
1199 let mut directory = root.try_clone()?;
1200 let mut prefix = PathBuf::new();
1201 let mut parts = path
1202 .components()
1203 .filter(|part| *part != Component::CurDir)
1204 .peekable();
1205 let mut hops = 0;
1206 while let Some(part) = parts.next() {
1207 prefix.push(part);
1208 let more = parts.peek().is_some();
1209 if !more && !follow_final {
1210 break;
1211 }
1212 work(1).map_err(PrefixError::Work)?;
1213 let metadata = match directory.symlink_metadata(part.as_os_str()) {
1214 Ok(metadata) => metadata,
1215 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
1216 Err(error) => return Err(error.into()),
1217 };
1218 if metadata.file_type().is_symlink() {
1219 hops += 1;
1220 if hops > MAX_LINK_HOPS {
1221 return Err(io::Error::other("too many levels of symbolic links").into());
1222 }
1223 work((prefix.components().count() as u64).saturating_mul(2))
1226 .map_err(PrefixError::Work)?;
1227 let resolved = match root.canonicalize(&prefix) {
1228 Ok(resolved) => resolved,
1229 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
1230 Err(error) => return Err(error.into()),
1231 };
1232 if protected_metadata(&resolved) {
1233 return Err(PrefixError::Filesystem(metadata_denied()));
1234 }
1235 if more {
1236 work(resolved.components().count() as u64 + 1).map_err(PrefixError::Work)?;
1237 directory = root.open_dir(&resolved)?;
1238 }
1239 } else if more {
1240 work(1).map_err(PrefixError::Work)?;
1241 directory = directory.open_dir_nofollow(part.as_os_str())?;
1242 }
1243 }
1244 Ok(())
1245}
1246
1247pub const MAX_REMOVE_ENTRIES: usize = 10_000;
1249
1250fn reject_metadata_descendants(
1251 dir: &Dir,
1252 count: &mut usize,
1253 depth: usize,
1254) -> Result<(), ContainError> {
1255 if depth > 64 {
1256 return Err(ContainError::Io(io::Error::other(
1257 "directory nesting limit exceeded",
1258 )));
1259 }
1260 for entry in dir.entries()? {
1261 *count += 1;
1262 if *count > MAX_REMOVE_ENTRIES {
1263 return Err(ContainError::Io(io::Error::other(
1264 "directory scan limit exceeded",
1265 )));
1266 }
1267 let entry = entry?;
1268 let name = entry.file_name();
1269 if protected_metadata(Path::new(&name)) {
1270 return Err(metadata_denied());
1271 }
1272 if dir.symlink_metadata(&name)?.is_dir() {
1273 reject_metadata_descendants(&dir.open_dir(name)?, count, depth + 1)?;
1274 }
1275 }
1276 Ok(())
1277}
1278
1279#[cfg(test)]
1280mod tests {
1281 use super::*;
1282
1283 const ROOT: &str = "/";
1284
1285 #[test]
1286 fn metadata_ancestors_use_one_component_handles() {
1287 let mut measured = Vec::new();
1288 for depth in [64, 128] {
1289 let temporary = tempfile::tempdir().unwrap();
1290 let relative = std::iter::repeat_n("d", depth)
1291 .collect::<Vec<_>>()
1292 .join("/");
1293 std::fs::create_dir_all(temporary.path().join(&relative)).unwrap();
1294 let root =
1295 Dir::open_ambient_dir(temporary.path(), cap_std::ambient_authority()).unwrap();
1296 let path = Path::new(&relative);
1297 let before = std::time::Instant::now();
1298 let mut previous_work = 0;
1299 for prefix in path
1300 .ancestors()
1301 .filter(|prefix| !prefix.as_os_str().is_empty())
1302 {
1303 root.canonicalize(prefix).unwrap();
1304 previous_work += prefix.components().count();
1305 }
1306 let old_time = before.elapsed();
1307 let before = std::time::Instant::now();
1308 let mut work = 0;
1309 check_metadata_prefixes(&root, path, true, &mut |units| {
1310 work += units;
1311 Ok::<_, std::convert::Infallible>(())
1312 })
1313 .unwrap();
1314 eprintln!(
1315 "metadata depth {depth}: old {previous_work} components/{old_time:?}, new {work} calls/{:?}",
1316 before.elapsed()
1317 );
1318 assert_eq!(previous_work, depth * (depth + 1) / 2);
1319 assert_eq!(work, 2 * depth as u64);
1320 assert!(work < previous_work as u64 / 2);
1321 measured.push(work);
1322 }
1323 assert_eq!(measured[1], measured[0] * 2);
1324 }
1325
1326 #[cfg(unix)]
1327 #[test]
1328 fn metadata_prefixes_protect_aliases_but_allow_unlinking_final_alias() {
1329 let temporary = tempfile::tempdir().unwrap();
1330 std::fs::create_dir_all(temporary.path().join("repo/.git")).unwrap();
1331 std::fs::create_dir(temporary.path().join("ordinary")).unwrap();
1332 std::os::unix::fs::symlink("repo/.git", temporary.path().join("metadata")).unwrap();
1333 std::os::unix::fs::symlink("ordinary", temporary.path().join("alias")).unwrap();
1334 let root = Dir::open_ambient_dir(temporary.path(), cap_std::ambient_authority()).unwrap();
1335 let mut work = |_| Ok::<_, std::convert::Infallible>(());
1336 let error = check_metadata_prefixes(&root, Path::new("metadata/config"), false, &mut work)
1337 .unwrap_err();
1338 assert!(matches!(error, PrefixError::Filesystem(_)));
1339 check_metadata_prefixes(&root, Path::new("metadata"), false, &mut work).unwrap();
1340 check_metadata_prefixes(&root, Path::new("alias/new"), false, &mut work).unwrap();
1341 }
1342
1343 fn rel(cwd: &str, path: &str) -> PathBuf {
1344 relative(cwd, path).expect("must resolve")
1345 }
1346
1347 #[test]
1348 fn absolute_path_resolves_under_root() {
1349 assert_eq!(rel(ROOT, "/foo/bar"), Path::new("foo/bar"));
1350 }
1351
1352 #[test]
1353 fn relative_path_resolves_under_cwd_root() {
1354 assert_eq!(rel(ROOT, "foo/bar"), Path::new("foo/bar"));
1355 }
1356
1357 #[test]
1358 fn bare_root_resolves_to_root() {
1359 assert_eq!(rel(ROOT, "/"), Path::new("."));
1360 }
1361
1362 #[test]
1363 fn empty_string_resolves_to_cwd() {
1364 assert_eq!(rel(ROOT, ""), Path::new("."));
1365 assert_eq!(rel("/home", ""), Path::new("home"));
1366 }
1367
1368 #[test]
1369 fn dot_segments_collapse() {
1370 assert_eq!(rel(ROOT, "/foo/./bar"), Path::new("foo/bar"));
1371 assert_eq!(rel(ROOT, "./foo"), Path::new("foo"));
1372 }
1373
1374 #[test]
1375 fn parent_within_root_is_ok() {
1376 assert_eq!(rel(ROOT, "/foo/../bar"), Path::new("bar"));
1377 assert_eq!(rel(ROOT, "foo/bar/../baz"), Path::new("foo/baz"));
1378 assert_eq!(rel(ROOT, "/foo/.."), Path::new("."));
1379 }
1380
1381 #[test]
1382 fn parent_above_root_is_escape() {
1383 assert_eq!(relative(ROOT, "/../etc/passwd"), Err(ResolveError::Escape));
1384 assert_eq!(relative(ROOT, ".."), Err(ResolveError::Escape));
1385 assert_eq!(relative(ROOT, "../foo"), Err(ResolveError::Escape));
1386 assert_eq!(relative(ROOT, "foo/../../bar"), Err(ResolveError::Escape));
1387 }
1388
1389 #[test]
1390 fn double_slashes_collapse() {
1391 assert_eq!(rel(ROOT, "//foo//bar"), Path::new("foo/bar"));
1392 }
1393
1394 #[test]
1395 fn nul_byte_rejected() {
1396 assert_eq!(relative(ROOT, "foo\0bar"), Err(ResolveError::EmbeddedNul));
1397 assert_eq!(relative("/ho\0me", "foo"), Err(ResolveError::EmbeddedNul));
1398 }
1399
1400 #[test]
1401 fn cwd_anchors_relative_paths() {
1402 assert_eq!(rel("/home", "foo"), Path::new("home/foo"));
1403 assert_eq!(
1404 rel("/home/user", "docs/a.txt"),
1405 Path::new("home/user/docs/a.txt"),
1406 );
1407 }
1408
1409 #[test]
1410 fn absolute_path_ignores_cwd() {
1411 assert_eq!(rel("/home", "/etc/passwd"), Path::new("etc/passwd"));
1412 }
1413
1414 #[test]
1415 fn parent_cancels_cwd_levels() {
1416 assert_eq!(rel("/home", ".."), Path::new("."));
1417 assert_eq!(rel("/home/user", "../other"), Path::new("home/other"));
1418 }
1419
1420 #[test]
1421 fn parent_escaping_through_cwd_is_escape() {
1422 assert_eq!(relative("/home", "../.."), Err(ResolveError::Escape));
1423 assert_eq!(
1424 relative("/home", "../../etc/passwd"),
1425 Err(ResolveError::Escape),
1426 );
1427 }
1428
1429 #[test]
1430 fn invalid_cwd_surfaces_as_escape() {
1431 assert_eq!(relative("..", "foo"), Err(ResolveError::Escape));
1432 }
1433
1434 #[test]
1435 fn cwd_without_leading_slash_treated_as_anchored() {
1436 assert_eq!(rel("home", "foo"), rel("/home", "foo"));
1437 }
1438
1439 #[test]
1440 fn guest_normalize_stays_in_guest_space() {
1441 assert_eq!(guest_normalize(ROOT, "/").unwrap(), "/");
1442 assert_eq!(guest_normalize(ROOT, "/tree/").unwrap(), "/tree");
1443 assert_eq!(guest_normalize(ROOT, "tree/sub/..").unwrap(), "/tree");
1444 assert_eq!(
1445 guest_normalize(ROOT, "tree/sub/file").unwrap(),
1446 "/tree/sub/file"
1447 );
1448 assert_eq!(
1449 guest_normalize(ROOT, "tree/./sub/../file").unwrap(),
1450 "/tree/file"
1451 );
1452 }
1453}
1454
1455#[cfg(test)]
1456mod contained_tests {
1457 use super::*;
1458
1459 const ROOT: &str = "/";
1460
1461 fn seeded() -> (tempfile::TempDir, Vfs) {
1462 let td = tempfile::tempdir().expect("tempdir");
1463 std::fs::create_dir_all(td.path().join("dir/sub")).expect("mkdir");
1464 std::fs::write(td.path().join("dir/a.txt"), b"inside").expect("write");
1465 let vfs = Vfs::external(td.path().to_path_buf()).expect("vfs");
1466 (td, vfs)
1467 }
1468
1469 #[test]
1470 fn bare_root_is_dot_not_empty() {
1471 assert_eq!(relative(ROOT, "/").unwrap(), Path::new("."));
1472 assert_eq!(relative(ROOT, "").unwrap(), Path::new("."));
1473 assert_eq!(relative(ROOT, "/foo/..").unwrap(), Path::new("."));
1474 }
1475
1476 #[test]
1477 fn resolved_paths_are_root_relative() {
1478 let rel = relative(ROOT, "/foo/bar").unwrap();
1479 assert_eq!(rel, Path::new("foo/bar"));
1480 assert!(!rel.is_absolute(), "must not carry a leading separator");
1481 }
1482
1483 #[test]
1484 fn trailing_slash_is_normalized_away() {
1485 assert_eq!(
1486 relative(ROOT, "/foo/").unwrap(),
1487 relative(ROOT, "/foo").unwrap()
1488 );
1489 assert_eq!(relative(ROOT, "foo//bar//").unwrap(), Path::new("foo/bar"));
1490 }
1491
1492 #[test]
1493 fn lexical_escape_keeps_its_existing_error() {
1494 assert_eq!(relative(ROOT, "../outside"), Err(ResolveError::Escape));
1495 assert_eq!(
1496 ResolveError::Escape.to_string(),
1497 "path escapes the VFS root",
1498 );
1499 assert_eq!(relative(ROOT, "a\0b"), Err(ResolveError::EmbeddedNul));
1500 }
1501
1502 #[test]
1503 fn content_form_reaches_the_full_path() {
1504 let (_td, vfs) = seeded();
1505 let p = resolve_content(&vfs, ROOT, "/dir/a.txt").expect("resolve");
1506 assert_eq!(p.read().expect("read"), b"inside");
1507 }
1508
1509 #[test]
1510 fn link_form_yields_the_parent_and_the_bare_final_component() {
1511 let (_td, vfs) = seeded();
1512 let p = resolve_link(&vfs, ROOT, "/dir/a.txt").expect("resolve");
1513 assert_eq!(p.name, std::ffi::OsStr::new("a.txt"));
1514 assert!(p.parent.metadata("a.txt").is_ok());
1516 }
1517
1518 #[test]
1519 fn link_form_on_the_root_names_dot() {
1520 let (_td, vfs) = seeded();
1521 let p = resolve_link(&vfs, ROOT, "/").expect("resolve");
1522 assert_eq!(p.name, std::ffi::OsStr::new("."));
1523 assert!(p.symlink_metadata().expect("stat root").is_dir());
1524 }
1525
1526 #[test]
1527 fn disabled_vfs_has_no_handle_to_resolve_against() {
1528 let vfs = Vfs::none();
1529 assert!(matches!(
1530 resolve_content(&vfs, ROOT, "/a"),
1531 Err(ContainError::Disabled)
1532 ));
1533 assert!(matches!(
1534 resolve_link(&vfs, ROOT, "/a"),
1535 Err(ContainError::Disabled)
1536 ));
1537 }
1538
1539 #[test]
1540 fn escape_classifier_separates_refusal_from_eacces() {
1541 let refusal = io::Error::new(io::ErrorKind::PermissionDenied, "escape");
1542 assert!(
1543 is_escape(&refusal),
1544 "constructed refusal carries no OS error"
1545 );
1546
1547 let eacces = io::Error::from_raw_os_error(13);
1548 assert_eq!(eacces.kind(), io::ErrorKind::PermissionDenied);
1549 assert!(
1550 !is_escape(&eacces),
1551 "a real EACCES must not read as an escape"
1552 );
1553
1554 assert!(!is_escape(&io::Error::from(io::ErrorKind::NotFound)));
1555 }
1556
1557 #[test]
1558 #[cfg(unix)]
1559 fn escape_classifier_matches_a_real_cap_std_refusal() {
1560 let (td, vfs) = seeded();
1561 let outside = td.path().parent().expect("parent");
1562 std::os::unix::fs::symlink(outside, td.path().join("escape")).expect("symlink");
1563
1564 let err = resolve_content(&vfs, ROOT, "/escape/whatever")
1565 .expect("lexically fine")
1566 .metadata()
1567 .expect_err("must refuse");
1568 assert!(
1569 matches!(err, ContainError::Escape),
1570 "a real cap-std escape must classify as Escape, got {err:?}",
1571 );
1572 assert_eq!(err.to_string(), "path escapes the VFS root");
1573 }
1574
1575 #[test]
1576 fn temp_sibling_stays_beside_its_target() {
1577 let (_td, vfs) = seeded();
1578 let p = resolve_content(&vfs, ROOT, "/dir/a.txt").expect("resolve");
1579 let tmp = p.temp_sibling();
1580 assert_eq!(tmp.rel.parent(), Some(Path::new("dir")));
1581 assert!(
1582 tmp.rel
1583 .file_name()
1584 .unwrap()
1585 .to_string_lossy()
1586 .starts_with("a.txt."),
1587 "temp name should derive from the target: {:?}",
1588 tmp.rel,
1589 );
1590 tmp.create().expect("create temp through the handle");
1591 assert!(_td.path().join(&tmp.rel).exists());
1592 }
1593}