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(crate) 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 pub fn temp_sibling(&self) -> Self {
361 let mut name = self
362 .rel
363 .file_name()
364 .map_or_else(|| OsString::from("file"), OsStr::to_os_string);
365 name.push(temp_suffix());
366 let mut rel = self.rel.parent().map(Path::to_path_buf).unwrap_or_default();
367 rel.push(name);
368 Self {
369 dir: Arc::clone(&self.dir),
370 rel,
371 placement: self.placement.clone(),
372 }
373 }
374}
375
376#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
380pub struct FileIdentity {
381 dev: u64,
382 ino: u64,
383}
384
385impl FileIdentity {
386 pub fn of(metadata: &Metadata) -> io::Result<Self> {
389 #[cfg(not(windows))]
390 {
391 Ok(Self {
392 dev: cap_fs_ext::MetadataExt::dev(metadata),
393 ino: cap_fs_ext::MetadataExt::ino(metadata),
394 })
395 }
396 #[cfg(windows)]
397 {
398 use cap_primitives::fs::_WindowsByHandle;
399 match (metadata.volume_serial_number(), metadata.file_index()) {
400 (Some(dev), Some(ino)) => Ok(Self {
401 dev: dev.into(),
402 ino,
403 }),
404 _ => Err(io::Error::other(
405 "a file's identity needs metadata read through an open handle",
406 )),
407 }
408 }
409 }
410}
411
412pub fn link_count(metadata: &Metadata) -> Option<u64> {
415 #[cfg(not(windows))]
416 {
417 Some(cap_fs_ext::MetadataExt::nlink(metadata))
418 }
419 #[cfg(windows)]
420 {
421 use cap_primitives::fs::_WindowsByHandle;
422 metadata.number_of_links().map(u64::from)
423 }
424}
425
426fn regular_file(metadata: Metadata) -> Option<(FileIdentity, u64)> {
429 if !metadata.is_file() {
430 return None;
431 }
432 let identity = FileIdentity::of(&metadata).ok()?;
433 Some((identity, metadata.len()))
434}
435
436fn require_regular_file(metadata: &Metadata) -> Result<(), ContainError> {
437 if metadata.is_file() {
438 return Ok(());
439 }
440 Err(io::Error::new(io::ErrorKind::InvalidInput, "not a regular file").into())
441}
442
443#[derive(Clone)]
453pub struct LinkPath {
454 parent: Arc<Dir>,
455 name: OsString,
456 guard: ContentPath,
457}
458
459impl fmt::Debug for LinkPath {
460 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461 f.debug_struct("LinkPath")
462 .field("name", &self.name)
463 .finish_non_exhaustive()
464 }
465}
466
467#[derive(Clone, Copy, Debug, PartialEq, Eq)]
470pub enum LinkKind {
471 File,
472 Directory,
473}
474
475impl LinkPath {
476 pub(crate) fn child(&self, parent: Arc<Dir>, name: OsString) -> Self {
477 let guard = ContentPath::new(
478 Arc::clone(&self.guard.dir),
479 self.guard.rel.join(&name),
480 self.guard.placement.clone(),
481 );
482 Self {
483 parent,
484 name,
485 guard,
486 }
487 }
488
489 pub fn is_root(&self) -> bool {
492 self.name == OsStr::new(".")
493 }
494
495 pub fn placement(&self) -> &Placement {
497 &self.guard.placement
498 }
499
500 pub fn writable(&self) -> Result<(), ContainError> {
502 self.guard.writable()
503 }
504
505 pub fn contains_mount_point(&self) -> Result<Option<Arc<str>>, ContainError> {
509 let placement = &self.guard.placement;
510 if placement.nested().is_empty() {
511 return Ok(None);
512 }
513 if let Some(mount) = mount_below(&self.guard.rel, placement) {
514 return Ok(Some(mount));
515 }
516 let real = real_location(&self.guard.dir, &self.guard.rel, false, placement)?;
517 Ok(mount_below(&real, placement))
518 }
519
520 pub fn same_entry(&self, other: &Self) -> Result<bool, ContainError> {
522 let source = self.symlink_metadata()?;
523 let target = match other.symlink_metadata() {
524 Ok(meta) => meta,
525 Err(ContainError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
526 return Ok(false);
527 }
528 Err(error) => return Err(error),
529 };
530 Ok(FileIdentity::of(&source)? == FileIdentity::of(&target)?)
531 }
532
533 pub fn check_copy_destination(&self, to: &ContentPath) -> Result<(), ContainError> {
535 let source = self.symlink_metadata()?;
536 let identity = FileIdentity::of(&source)?;
537 let invalid = || {
538 ContainError::Io(io::Error::new(
539 io::ErrorKind::InvalidInput,
540 "cannot copy an entry onto itself or a directory into its own tree",
541 ))
542 };
543 if source.is_dir() && to.placement.ancestors().contains(&identity) {
544 return Err(invalid());
545 }
546 let mut prefix = PathBuf::from(".");
547 let parts = std::iter::once(None).chain(to.rel.components().map(Some));
548 for part in parts {
549 if let Some(part) = part {
550 prefix.push(part.as_os_str());
551 }
552 match to.dir.metadata(&prefix) {
553 Ok(meta) => {
554 if FileIdentity::of(&meta)? == identity
555 && (source.is_dir()
556 || prefix == to.rel
557 || prefix.strip_prefix(".").is_ok_and(|p| p == to.rel))
558 {
559 return Err(invalid());
560 }
561 }
562 Err(error)
563 if matches!(
564 error.kind(),
565 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
566 ) =>
567 {
568 break;
569 }
570 Err(error) => return Err(error.into()),
571 }
572 }
573 Ok(())
574 }
575
576 pub fn temp_sibling(&self) -> Self {
579 let mut name = self.name.clone();
580 name.push(temp_suffix());
581 let rel = self
582 .guard
583 .rel
584 .parent()
585 .map_or_else(|| PathBuf::from(&name), |parent| parent.join(&name));
586 Self {
587 parent: Arc::clone(&self.parent),
588 name,
589 guard: ContentPath::new(
590 Arc::clone(&self.guard.dir),
591 rel,
592 self.guard.placement.clone(),
593 ),
594 }
595 }
596
597 pub fn symlink_metadata(&self) -> Result<Metadata, ContainError> {
598 Ok(self.parent.symlink_metadata(&self.name)?)
599 }
600
601 pub fn read_link_contents(&self) -> Result<PathBuf, ContainError> {
602 Ok(self.parent.read_link_contents(&self.name)?)
603 }
604
605 pub fn link_kind(&self) -> Result<LinkKind, ContainError> {
610 #[cfg(not(windows))]
611 {
612 Ok(LinkKind::File)
613 }
614 #[cfg(windows)]
615 {
616 use cap_std::fs::MetadataExt;
617
618 const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
619 let attrs = self.symlink_metadata()?.file_attributes();
620 Ok(if attrs & FILE_ATTRIBUTE_DIRECTORY == 0 {
621 LinkKind::File
622 } else {
623 LinkKind::Directory
624 })
625 }
626 }
627
628 pub fn symlink(&self, target: &Path, kind: LinkKind) -> Result<(), ContainError> {
643 self.guard.check_link_mutation(false)?;
644 #[cfg(not(windows))]
645 {
646 let _ = kind;
647 Ok(self.parent.symlink_contents(target, &self.name)?)
648 }
649 #[cfg(windows)]
650 {
651 match kind {
652 LinkKind::Directory => Ok(DirExt::symlink_dir(&*self.parent, target, &self.name)?),
653 LinkKind::File => Ok(DirExt::symlink_file(&*self.parent, target, &self.name)?),
654 }
655 }
656 }
657
658 pub fn remove_file(&self) -> Result<(), ContainError> {
659 self.guard.check_link_mutation(false)?;
660 Ok(self.parent.remove_file_or_symlink(&self.name)?)
663 }
664
665 pub fn remove_dir(&self) -> Result<(), ContainError> {
666 self.guard.check_link_mutation(true)?;
667 Ok(self.parent.remove_dir(&self.name)?)
668 }
669
670 pub fn remove_dir_all(&self) -> Result<(), ContainError> {
671 self.check_removable()?;
672 Ok(self.parent.remove_dir_all(&self.name)?)
673 }
674
675 pub fn check_removable(&self) -> Result<(), ContainError> {
679 self.guard.check_link_mutation(true)
680 }
681
682 pub fn create_dir_all(&self) -> Result<(), ContainError> {
683 if self
684 .parent
685 .metadata(&self.name)
686 .is_ok_and(|meta| meta.is_dir())
687 {
688 return Ok(());
689 }
690 self.guard.check_link_mutation(false)?;
691 match self.parent.create_dir(&self.name) {
692 Ok(()) => Ok(()),
693 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
694 Err(e) => Err(e.into()),
695 }
696 }
697
698 pub fn open_dir(&self) -> Result<Dir, ContainError> {
701 Ok(self.parent.open_dir(&self.name)?)
702 }
703
704 pub fn regular_file(&self) -> Option<(FileIdentity, u64)> {
707 regular_file(self.symlink_metadata().ok()?)
708 }
709
710 pub fn copied_onto_file(&self) -> Option<(FileIdentity, u64)> {
713 regular_file(self.parent.metadata(&self.name).ok()?)
714 }
715
716 pub fn entries(&self) -> Result<ReadDir, ContainError> {
717 Ok(self.parent.read_dir(&self.name)?)
718 }
719
720 pub fn copy_to(&self, dest: &Self) -> Result<(), ContainError> {
724 self.copy_to_counted(dest).map(|_| ())
725 }
726
727 pub(crate) fn copy_to_counted(&self, dest: &Self) -> Result<u64, ContainError> {
728 dest.guard.check_mutation(true)?;
729 Ok(self.parent.copy(&self.name, &dest.parent, &dest.name)?)
730 }
731
732 pub fn rename_to(&self, dest: &Self) -> Result<(), ContainError> {
735 self.check_removable()?;
736 dest.check_removable()?;
737 Ok(self.parent.rename(&self.name, &dest.parent, &dest.name)?)
738 }
739}
740
741fn temp_suffix() -> String {
746 static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
747 let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
748 let pid = std::process::id();
749 let nanos = std::time::SystemTime::now()
750 .duration_since(std::time::UNIX_EPOCH)
751 .map_or(0, |d| d.as_nanos());
752 format!(".{nanos}.{pid}.{seq}.tmp")
753}
754
755pub fn resolve_content(
760 vfs: &Vfs,
761 cwd: &str,
762 guest_path: &str,
763) -> Result<ContentPath, ContainError> {
764 vfs.dir().ok_or(ContainError::Disabled)?;
766 let rel = relative(cwd, guest_path)?;
767 let (dir, rel, placement) = vfs.locate(&rel).ok_or(ContainError::Disabled)?;
768 Ok(ContentPath::new(dir, rel, placement))
769}
770
771pub fn resolve_link(vfs: &Vfs, cwd: &str, guest_path: &str) -> Result<LinkPath, ContainError> {
780 vfs.dir().ok_or(ContainError::Disabled)?;
782 let rel = relative(cwd, guest_path)?;
783 let (root, rel, placement) = vfs.locate(&rel).ok_or(ContainError::Disabled)?;
784 let Some(name) = rel.file_name() else {
785 let name = OsString::from(".");
786 let guard = ContentPath::new(Arc::clone(&root), PathBuf::from(&name), placement);
787 return Ok(LinkPath {
788 parent: root,
789 name,
790 guard,
791 });
792 };
793 let name = name.to_os_string();
794 let parent_rel = rel.parent().unwrap_or(Path::new(""));
795 let parent = if parent_rel.as_os_str().is_empty() {
796 Arc::clone(&root)
797 } else {
798 Arc::new(root.open_dir(parent_rel)?)
799 };
800 Ok(LinkPath {
801 parent,
802 name,
803 guard: ContentPath::new(root, rel, placement),
804 })
805}
806
807pub fn guest_normalize(cwd: &str, guest_path: &str) -> Result<String, ResolveError> {
812 let rel = relative(cwd, guest_path)?;
813 if rel == Path::new(".") {
814 return Ok(String::from("/"));
815 }
816 let components = rel
818 .components()
819 .map(|component| component.as_os_str().to_string_lossy())
820 .collect::<Vec<_>>();
821 Ok(format!("/{}", components.join("/")))
822}
823
824fn relative(cwd: &str, guest_path: &str) -> Result<PathBuf, ResolveError> {
828 if guest_path.as_bytes().contains(&0) || cwd.as_bytes().contains(&0) {
829 return Err(ResolveError::EmbeddedNul);
830 }
831 let path = Path::new(guest_path);
832 let is_absolute = matches!(path.components().next(), Some(Component::RootDir));
833
834 let mut stack: Vec<OsString> = Vec::new();
835 if !is_absolute {
836 for comp in Path::new(cwd).components() {
837 push_component(&mut stack, comp)?;
838 }
839 }
840 for comp in path.components() {
841 push_component(&mut stack, comp)?;
842 }
843
844 if stack.is_empty() {
845 return Ok(PathBuf::from("."));
846 }
847 let mut out = PathBuf::new();
848 for name in &stack {
849 out.push(name);
850 }
851 Ok(out)
852}
853
854pub(crate) fn protected_metadata(path: &Path) -> bool {
855 path.components().any(|part| {
856 let name = part.as_os_str().to_string_lossy();
857 let options = gix::validate::path::component::Options {
858 protect_windows: false,
859 ..Default::default()
860 };
861 let reserved_alias = matches!(
862 gix::validate::path::component(name.as_bytes().into(), None, options),
863 Err(gix::validate::path::component::Error::DotGitDir)
864 );
865 reserved_alias
866 || name
867 .trim_end_matches([' ', '.'])
868 .eq_ignore_ascii_case(".git")
869 || name.to_ascii_lowercase().starts_with(".git-submilli-")
870 })
871}
872
873pub(crate) fn check_repository_clear_of_mounts(
879 root: &Dir,
880 path: &Path,
881 placement: &Placement,
882) -> Result<(), ContainError> {
883 check_nested_mount_points(root, path, placement, true, true)?;
884 match root.metadata(path) {
885 Ok(meta) if meta.is_dir() => {
886 if placement.protects_descendants(dir_identity(root, path, &meta)?) {
887 return Err(ContainError::MountPoint(Arc::from(placement.mount_point())));
888 }
889 }
890 Ok(_) => {}
891 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
892 Err(error) => return Err(error.into()),
893 }
894 Ok(())
895}
896
897fn check_mount_points(
906 root: &Dir,
907 path: &Path,
908 placement: &Placement,
909 recursive: bool,
910 follow_final: bool,
911) -> Result<(), ContainError> {
912 check_nested_mount_points(root, path, placement, recursive, follow_final)?;
913 let metadata = if follow_final {
914 root.metadata(path)
915 } else {
916 root.symlink_metadata(path)
917 };
918 match metadata {
919 Ok(meta) if meta.is_dir() => {
920 if placement.protects(dir_identity(root, path, &meta)?, recursive) {
921 return Err(ContainError::MountPoint(Arc::from(placement.mount_point())));
922 }
923 }
924 Ok(_) => {}
925 Err(error)
926 if matches!(
927 error.kind(),
928 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
929 ) => {}
930 Err(error) => return Err(error.into()),
931 }
932 Ok(())
933}
934
935fn check_nested_mount_points(
936 root: &Dir,
937 path: &Path,
938 placement: &Placement,
939 recursive: bool,
940 follow_final: bool,
941) -> Result<(), ContainError> {
942 if placement.nested().is_empty() {
943 return Ok(());
944 }
945 if let Some(mount) = mount_at_or_below(path, placement, recursive) {
946 return Err(ContainError::MountPoint(mount));
947 }
948 let real = real_location(root, path, follow_final, placement)?;
949 if let Some(mount) = mount_at_or_below(&real, placement, recursive) {
950 return Err(ContainError::MountPoint(mount));
951 }
952 Ok(())
953}
954
955fn real_location(
967 root: &Dir,
968 path: &Path,
969 follow_final: bool,
970 placement: &Placement,
971) -> Result<PathBuf, ContainError> {
972 let mut pending: Vec<OsString> = Vec::new();
973 push_reversed(&mut pending, path);
974 let mut resolved = PathBuf::new();
975 let mut missing = false;
976 let mut hops = 0usize;
977 while let Some(name) = pending.pop() {
978 if name == OsStr::new(".") {
979 continue;
980 }
981 if name == OsStr::new("..") {
982 if !resolved.pop() {
983 return Err(ContainError::Escape);
984 }
985 continue;
986 }
987 let candidate = resolved.join(&name);
988 if missing {
989 resolved = candidate;
990 continue;
991 }
992 let follow_this = follow_final || !pending.is_empty();
993 match root.symlink_metadata(&candidate) {
994 Ok(meta) if meta.file_type().is_symlink() && follow_this => {
995 hops += 1;
996 if hops > MAX_LINK_HOPS {
997 return Err(ContainError::Io(io::Error::other(
998 "too many levels of symbolic links",
999 )));
1000 }
1001 let target = root.read_link_contents(&candidate)?;
1002 if target.has_root() {
1003 return Err(ContainError::Escape);
1004 }
1005 push_reversed(&mut pending, &target);
1006 }
1007 Ok(meta) => match placeholder_name(placement, root, &candidate, &resolved, &meta) {
1008 Ok(Some(exact)) => resolved.push(exact),
1009 Ok(None) => resolved = candidate,
1010 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1012 missing = true;
1013 resolved = candidate;
1014 }
1015 Err(error) => return Err(error.into()),
1016 },
1017 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1018 missing = true;
1019 resolved = candidate;
1020 }
1021 Err(error) => return Err(error.into()),
1022 }
1023 }
1024 Ok(resolved)
1025}
1026
1027fn placeholder_name<'a>(
1035 placement: &'a Placement,
1036 root: &Dir,
1037 path: &Path,
1038 parent: &Path,
1039 meta: &Metadata,
1040) -> io::Result<Option<&'a OsStr>> {
1041 if !meta.is_dir() {
1042 return Ok(None);
1043 }
1044 let mut beside = placement
1045 .nested()
1046 .iter()
1047 .flat_map(Mount::placeholders)
1048 .filter(|placeholder| placeholder.parent == parent)
1049 .peekable();
1050 if beside.peek().is_none() {
1051 return Ok(None);
1052 }
1053 let identity = dir_identity(root, path, meta)?;
1054 Ok(beside
1055 .find(|placeholder| placeholder.identity == identity)
1056 .map(|placeholder| placeholder.name.as_os_str()))
1057}
1058
1059pub(crate) fn dir_identity(root: &Dir, path: &Path, meta: &Metadata) -> io::Result<FileIdentity> {
1063 match FileIdentity::of(meta) {
1064 Ok(identity) => Ok(identity),
1065 Err(_) => FileIdentity::of(&root.open_dir(path)?.dir_metadata()?),
1066 }
1067}
1068
1069fn push_reversed(pending: &mut Vec<OsString>, path: &Path) {
1071 pending.extend(
1072 path.components()
1073 .rev()
1074 .map(|part| part.as_os_str().to_os_string()),
1075 );
1076}
1077
1078const MAX_LINK_HOPS: usize = 40;
1080
1081fn mount_at_or_below(path: &Path, placement: &Placement, recursive: bool) -> Option<Arc<str>> {
1083 placement.nested().iter().find_map(|mount| {
1084 let inside = starts_with_component(path, mount.rel());
1085 let above = recursive && starts_with_component(mount.rel(), path);
1086 (inside || above).then(|| Arc::from(mount.guest_path()))
1087 })
1088}
1089
1090fn mount_below(path: &Path, placement: &Placement) -> Option<Arc<str>> {
1092 placement.nested().iter().find_map(|mount| {
1093 let below = starts_with_component(mount.rel(), path) && mount.rel() != path;
1094 below.then(|| Arc::from(mount.guest_path()))
1095 })
1096}
1097
1098fn starts_with_component(path: &Path, prefix: &Path) -> bool {
1102 if prefix == Path::new(".") {
1103 return true;
1104 }
1105 let mut parts = path.components().filter(|c| *c != Component::CurDir);
1106 for want in prefix.components().filter(|c| *c != Component::CurDir) {
1107 match parts.next() {
1108 Some(have) if same_component(have.as_os_str(), want.as_os_str()) => {}
1109 _ => return false,
1110 }
1111 }
1112 true
1113}
1114
1115fn same_component(a: &OsStr, b: &OsStr) -> bool {
1116 if cfg!(any(target_os = "macos", windows)) {
1117 a.to_string_lossy()
1118 .eq_ignore_ascii_case(&b.to_string_lossy())
1119 } else {
1120 a == b
1121 }
1122}
1123
1124fn metadata_denied() -> ContainError {
1125 ContainError::Io(io::Error::other(
1126 "Git metadata is protected; use submilli:git with Git capabilities",
1127 ))
1128}
1129
1130fn check_metadata_mutation(
1131 root: &Dir,
1132 path: &Path,
1133 recursive: bool,
1134 follow_final: bool,
1135) -> Result<(), ContainError> {
1136 if protected_metadata(path) {
1137 return Err(metadata_denied());
1138 }
1139 match check_metadata_prefixes(root, path, follow_final, &mut |_| {
1140 Ok::<_, std::convert::Infallible>(())
1141 }) {
1142 Ok(()) => {}
1143 Err(PrefixError::Filesystem(error)) => return Err(error),
1144 Err(PrefixError::Work(never)) => match never {},
1145 }
1146 match root.symlink_metadata(path) {
1147 Ok(meta) => {
1148 #[cfg(unix)]
1149 {
1150 use cap_std::fs::MetadataExt;
1151 if meta.is_file() && meta.nlink() > 1 {
1152 return Err(metadata_denied());
1153 }
1154 }
1155 if recursive && meta.is_dir() {
1156 reject_metadata_descendants(&root.open_dir(path)?, &mut 0, 0)?;
1157 }
1158 }
1159 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
1160 Err(error) => return Err(error.into()),
1161 }
1162 Ok(())
1163}
1164
1165#[derive(Debug)]
1166enum PrefixError<E> {
1167 Filesystem(ContainError),
1168 Work(E),
1169}
1170
1171impl<E> From<io::Error> for PrefixError<E> {
1172 fn from(error: io::Error) -> Self {
1173 Self::Filesystem(error.into())
1174 }
1175}
1176
1177fn check_metadata_prefixes<E>(
1178 root: &Dir,
1179 path: &Path,
1180 follow_final: bool,
1181 work: &mut impl FnMut(u64) -> Result<(), E>,
1182) -> Result<(), PrefixError<E>> {
1183 if protected_metadata(path) {
1184 return Err(PrefixError::Filesystem(metadata_denied()));
1185 }
1186 work(1).map_err(PrefixError::Work)?;
1187 let mut directory = root.try_clone()?;
1188 let mut prefix = PathBuf::new();
1189 let mut parts = path
1190 .components()
1191 .filter(|part| *part != Component::CurDir)
1192 .peekable();
1193 let mut hops = 0;
1194 while let Some(part) = parts.next() {
1195 prefix.push(part);
1196 let more = parts.peek().is_some();
1197 if !more && !follow_final {
1198 break;
1199 }
1200 work(1).map_err(PrefixError::Work)?;
1201 let metadata = match directory.symlink_metadata(part.as_os_str()) {
1202 Ok(metadata) => metadata,
1203 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
1204 Err(error) => return Err(error.into()),
1205 };
1206 if metadata.file_type().is_symlink() {
1207 hops += 1;
1208 if hops > MAX_LINK_HOPS {
1209 return Err(io::Error::other("too many levels of symbolic links").into());
1210 }
1211 work((prefix.components().count() as u64).saturating_mul(2))
1214 .map_err(PrefixError::Work)?;
1215 let resolved = match root.canonicalize(&prefix) {
1216 Ok(resolved) => resolved,
1217 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
1218 Err(error) => return Err(error.into()),
1219 };
1220 if protected_metadata(&resolved) {
1221 return Err(PrefixError::Filesystem(metadata_denied()));
1222 }
1223 if more {
1224 work(resolved.components().count() as u64 + 1).map_err(PrefixError::Work)?;
1225 directory = root.open_dir(&resolved)?;
1226 }
1227 } else if more {
1228 work(1).map_err(PrefixError::Work)?;
1229 directory = directory.open_dir_nofollow(part.as_os_str())?;
1230 }
1231 }
1232 Ok(())
1233}
1234
1235pub const MAX_REMOVE_ENTRIES: usize = 10_000;
1237
1238fn reject_metadata_descendants(
1239 dir: &Dir,
1240 count: &mut usize,
1241 depth: usize,
1242) -> Result<(), ContainError> {
1243 if depth > 64 {
1244 return Err(ContainError::Io(io::Error::other(
1245 "directory nesting limit exceeded",
1246 )));
1247 }
1248 for entry in dir.entries()? {
1249 *count += 1;
1250 if *count > MAX_REMOVE_ENTRIES {
1251 return Err(ContainError::Io(io::Error::other(
1252 "directory scan limit exceeded",
1253 )));
1254 }
1255 let entry = entry?;
1256 let name = entry.file_name();
1257 if protected_metadata(Path::new(&name)) {
1258 return Err(metadata_denied());
1259 }
1260 if dir.symlink_metadata(&name)?.is_dir() {
1261 reject_metadata_descendants(&dir.open_dir(name)?, count, depth + 1)?;
1262 }
1263 }
1264 Ok(())
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269 use super::*;
1270
1271 const ROOT: &str = "/";
1272
1273 #[test]
1274 fn metadata_ancestors_use_one_component_handles() {
1275 let mut measured = Vec::new();
1276 for depth in [64, 128] {
1277 let temporary = tempfile::tempdir().unwrap();
1278 let relative = std::iter::repeat_n("d", depth)
1279 .collect::<Vec<_>>()
1280 .join("/");
1281 std::fs::create_dir_all(temporary.path().join(&relative)).unwrap();
1282 let root =
1283 Dir::open_ambient_dir(temporary.path(), cap_std::ambient_authority()).unwrap();
1284 let path = Path::new(&relative);
1285 let before = std::time::Instant::now();
1286 let mut previous_work = 0;
1287 for prefix in path
1288 .ancestors()
1289 .filter(|prefix| !prefix.as_os_str().is_empty())
1290 {
1291 root.canonicalize(prefix).unwrap();
1292 previous_work += prefix.components().count();
1293 }
1294 let old_time = before.elapsed();
1295 let before = std::time::Instant::now();
1296 let mut work = 0;
1297 check_metadata_prefixes(&root, path, true, &mut |units| {
1298 work += units;
1299 Ok::<_, std::convert::Infallible>(())
1300 })
1301 .unwrap();
1302 eprintln!(
1303 "metadata depth {depth}: old {previous_work} components/{old_time:?}, new {work} calls/{:?}",
1304 before.elapsed()
1305 );
1306 assert_eq!(previous_work, depth * (depth + 1) / 2);
1307 assert_eq!(work, 2 * depth as u64);
1308 assert!(work < previous_work as u64 / 2);
1309 measured.push(work);
1310 }
1311 assert_eq!(measured[1], measured[0] * 2);
1312 }
1313
1314 #[cfg(unix)]
1315 #[test]
1316 fn metadata_prefixes_protect_aliases_but_allow_unlinking_final_alias() {
1317 let temporary = tempfile::tempdir().unwrap();
1318 std::fs::create_dir_all(temporary.path().join("repo/.git")).unwrap();
1319 std::fs::create_dir(temporary.path().join("ordinary")).unwrap();
1320 std::os::unix::fs::symlink("repo/.git", temporary.path().join("metadata")).unwrap();
1321 std::os::unix::fs::symlink("ordinary", temporary.path().join("alias")).unwrap();
1322 let root = Dir::open_ambient_dir(temporary.path(), cap_std::ambient_authority()).unwrap();
1323 let mut work = |_| Ok::<_, std::convert::Infallible>(());
1324 let error = check_metadata_prefixes(&root, Path::new("metadata/config"), false, &mut work)
1325 .unwrap_err();
1326 assert!(matches!(error, PrefixError::Filesystem(_)));
1327 check_metadata_prefixes(&root, Path::new("metadata"), false, &mut work).unwrap();
1328 check_metadata_prefixes(&root, Path::new("alias/new"), false, &mut work).unwrap();
1329 }
1330
1331 fn rel(cwd: &str, path: &str) -> PathBuf {
1332 relative(cwd, path).expect("must resolve")
1333 }
1334
1335 #[test]
1336 fn absolute_path_resolves_under_root() {
1337 assert_eq!(rel(ROOT, "/foo/bar"), Path::new("foo/bar"));
1338 }
1339
1340 #[test]
1341 fn relative_path_resolves_under_cwd_root() {
1342 assert_eq!(rel(ROOT, "foo/bar"), Path::new("foo/bar"));
1343 }
1344
1345 #[test]
1346 fn bare_root_resolves_to_root() {
1347 assert_eq!(rel(ROOT, "/"), Path::new("."));
1348 }
1349
1350 #[test]
1351 fn empty_string_resolves_to_cwd() {
1352 assert_eq!(rel(ROOT, ""), Path::new("."));
1353 assert_eq!(rel("/home", ""), Path::new("home"));
1354 }
1355
1356 #[test]
1357 fn dot_segments_collapse() {
1358 assert_eq!(rel(ROOT, "/foo/./bar"), Path::new("foo/bar"));
1359 assert_eq!(rel(ROOT, "./foo"), Path::new("foo"));
1360 }
1361
1362 #[test]
1363 fn parent_within_root_is_ok() {
1364 assert_eq!(rel(ROOT, "/foo/../bar"), Path::new("bar"));
1365 assert_eq!(rel(ROOT, "foo/bar/../baz"), Path::new("foo/baz"));
1366 assert_eq!(rel(ROOT, "/foo/.."), Path::new("."));
1367 }
1368
1369 #[test]
1370 fn parent_above_root_is_escape() {
1371 assert_eq!(relative(ROOT, "/../etc/passwd"), Err(ResolveError::Escape));
1372 assert_eq!(relative(ROOT, ".."), Err(ResolveError::Escape));
1373 assert_eq!(relative(ROOT, "../foo"), Err(ResolveError::Escape));
1374 assert_eq!(relative(ROOT, "foo/../../bar"), Err(ResolveError::Escape));
1375 }
1376
1377 #[test]
1378 fn double_slashes_collapse() {
1379 assert_eq!(rel(ROOT, "//foo//bar"), Path::new("foo/bar"));
1380 }
1381
1382 #[test]
1383 fn nul_byte_rejected() {
1384 assert_eq!(relative(ROOT, "foo\0bar"), Err(ResolveError::EmbeddedNul));
1385 assert_eq!(relative("/ho\0me", "foo"), Err(ResolveError::EmbeddedNul));
1386 }
1387
1388 #[test]
1389 fn cwd_anchors_relative_paths() {
1390 assert_eq!(rel("/home", "foo"), Path::new("home/foo"));
1391 assert_eq!(
1392 rel("/home/user", "docs/a.txt"),
1393 Path::new("home/user/docs/a.txt"),
1394 );
1395 }
1396
1397 #[test]
1398 fn absolute_path_ignores_cwd() {
1399 assert_eq!(rel("/home", "/etc/passwd"), Path::new("etc/passwd"));
1400 }
1401
1402 #[test]
1403 fn parent_cancels_cwd_levels() {
1404 assert_eq!(rel("/home", ".."), Path::new("."));
1405 assert_eq!(rel("/home/user", "../other"), Path::new("home/other"));
1406 }
1407
1408 #[test]
1409 fn parent_escaping_through_cwd_is_escape() {
1410 assert_eq!(relative("/home", "../.."), Err(ResolveError::Escape));
1411 assert_eq!(
1412 relative("/home", "../../etc/passwd"),
1413 Err(ResolveError::Escape),
1414 );
1415 }
1416
1417 #[test]
1418 fn invalid_cwd_surfaces_as_escape() {
1419 assert_eq!(relative("..", "foo"), Err(ResolveError::Escape));
1420 }
1421
1422 #[test]
1423 fn cwd_without_leading_slash_treated_as_anchored() {
1424 assert_eq!(rel("home", "foo"), rel("/home", "foo"));
1425 }
1426
1427 #[test]
1428 fn guest_normalize_stays_in_guest_space() {
1429 assert_eq!(guest_normalize(ROOT, "/").unwrap(), "/");
1430 assert_eq!(guest_normalize(ROOT, "/tree/").unwrap(), "/tree");
1431 assert_eq!(guest_normalize(ROOT, "tree/sub/..").unwrap(), "/tree");
1432 assert_eq!(
1433 guest_normalize(ROOT, "tree/sub/file").unwrap(),
1434 "/tree/sub/file"
1435 );
1436 assert_eq!(
1437 guest_normalize(ROOT, "tree/./sub/../file").unwrap(),
1438 "/tree/file"
1439 );
1440 }
1441}
1442
1443#[cfg(test)]
1444mod contained_tests {
1445 use super::*;
1446
1447 const ROOT: &str = "/";
1448
1449 fn seeded() -> (tempfile::TempDir, Vfs) {
1450 let td = tempfile::tempdir().expect("tempdir");
1451 std::fs::create_dir_all(td.path().join("dir/sub")).expect("mkdir");
1452 std::fs::write(td.path().join("dir/a.txt"), b"inside").expect("write");
1453 let vfs = Vfs::external(td.path().to_path_buf()).expect("vfs");
1454 (td, vfs)
1455 }
1456
1457 #[test]
1458 fn bare_root_is_dot_not_empty() {
1459 assert_eq!(relative(ROOT, "/").unwrap(), Path::new("."));
1460 assert_eq!(relative(ROOT, "").unwrap(), Path::new("."));
1461 assert_eq!(relative(ROOT, "/foo/..").unwrap(), Path::new("."));
1462 }
1463
1464 #[test]
1465 fn resolved_paths_are_root_relative() {
1466 let rel = relative(ROOT, "/foo/bar").unwrap();
1467 assert_eq!(rel, Path::new("foo/bar"));
1468 assert!(!rel.is_absolute(), "must not carry a leading separator");
1469 }
1470
1471 #[test]
1472 fn trailing_slash_is_normalized_away() {
1473 assert_eq!(
1474 relative(ROOT, "/foo/").unwrap(),
1475 relative(ROOT, "/foo").unwrap()
1476 );
1477 assert_eq!(relative(ROOT, "foo//bar//").unwrap(), Path::new("foo/bar"));
1478 }
1479
1480 #[test]
1481 fn lexical_escape_keeps_its_existing_error() {
1482 assert_eq!(relative(ROOT, "../outside"), Err(ResolveError::Escape));
1483 assert_eq!(
1484 ResolveError::Escape.to_string(),
1485 "path escapes the VFS root",
1486 );
1487 assert_eq!(relative(ROOT, "a\0b"), Err(ResolveError::EmbeddedNul));
1488 }
1489
1490 #[test]
1491 fn content_form_reaches_the_full_path() {
1492 let (_td, vfs) = seeded();
1493 let p = resolve_content(&vfs, ROOT, "/dir/a.txt").expect("resolve");
1494 assert_eq!(p.read().expect("read"), b"inside");
1495 }
1496
1497 #[test]
1498 fn link_form_yields_the_parent_and_the_bare_final_component() {
1499 let (_td, vfs) = seeded();
1500 let p = resolve_link(&vfs, ROOT, "/dir/a.txt").expect("resolve");
1501 assert_eq!(p.name, std::ffi::OsStr::new("a.txt"));
1502 assert!(p.parent.metadata("a.txt").is_ok());
1504 }
1505
1506 #[test]
1507 fn link_form_on_the_root_names_dot() {
1508 let (_td, vfs) = seeded();
1509 let p = resolve_link(&vfs, ROOT, "/").expect("resolve");
1510 assert_eq!(p.name, std::ffi::OsStr::new("."));
1511 assert!(p.symlink_metadata().expect("stat root").is_dir());
1512 }
1513
1514 #[test]
1515 fn disabled_vfs_has_no_handle_to_resolve_against() {
1516 let vfs = Vfs::none();
1517 assert!(matches!(
1518 resolve_content(&vfs, ROOT, "/a"),
1519 Err(ContainError::Disabled)
1520 ));
1521 assert!(matches!(
1522 resolve_link(&vfs, ROOT, "/a"),
1523 Err(ContainError::Disabled)
1524 ));
1525 }
1526
1527 #[test]
1528 fn escape_classifier_separates_refusal_from_eacces() {
1529 let refusal = io::Error::new(io::ErrorKind::PermissionDenied, "escape");
1530 assert!(
1531 is_escape(&refusal),
1532 "constructed refusal carries no OS error"
1533 );
1534
1535 let eacces = io::Error::from_raw_os_error(13);
1536 assert_eq!(eacces.kind(), io::ErrorKind::PermissionDenied);
1537 assert!(
1538 !is_escape(&eacces),
1539 "a real EACCES must not read as an escape"
1540 );
1541
1542 assert!(!is_escape(&io::Error::from(io::ErrorKind::NotFound)));
1543 }
1544
1545 #[test]
1546 #[cfg(unix)]
1547 fn escape_classifier_matches_a_real_cap_std_refusal() {
1548 let (td, vfs) = seeded();
1549 let outside = td.path().parent().expect("parent");
1550 std::os::unix::fs::symlink(outside, td.path().join("escape")).expect("symlink");
1551
1552 let err = resolve_content(&vfs, ROOT, "/escape/whatever")
1553 .expect("lexically fine")
1554 .metadata()
1555 .expect_err("must refuse");
1556 assert!(
1557 matches!(err, ContainError::Escape),
1558 "a real cap-std escape must classify as Escape, got {err:?}",
1559 );
1560 assert_eq!(err.to_string(), "path escapes the VFS root");
1561 }
1562
1563 #[test]
1564 fn temp_sibling_stays_beside_its_target() {
1565 let (_td, vfs) = seeded();
1566 let p = resolve_content(&vfs, ROOT, "/dir/a.txt").expect("resolve");
1567 let tmp = p.temp_sibling();
1568 assert_eq!(tmp.rel.parent(), Some(Path::new("dir")));
1569 assert!(
1570 tmp.rel
1571 .file_name()
1572 .unwrap()
1573 .to_string_lossy()
1574 .starts_with("a.txt."),
1575 "temp name should derive from the target: {:?}",
1576 tmp.rel,
1577 );
1578 tmp.create().expect("create temp through the handle");
1579 assert!(_td.path().join(&tmp.rel).exists());
1580 }
1581}