1use std::ffi::{OsStr, OsString};
18use std::fmt;
19use std::io::{self, Read};
20use std::path::{Component, Path, PathBuf};
21use std::sync::Arc;
22
23use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt};
24use cap_std::ambient_authority;
25use cap_std::fs::Dir;
26use tempfile::TempDir;
27
28use crate::runtime::disk_quota::DiskQuota;
29use crate::runtime::fs::{FileIdentity, dir_identity};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35pub enum VfsMode {
36 None,
37 Ephemeral,
38 PerSession,
39 Named,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44pub enum Access {
45 ReadOnly,
46 ReadWrite,
47}
48
49impl Access {
50 pub fn as_str(self) -> &'static str {
51 match self {
52 Self::ReadOnly => "read_only",
53 Self::ReadWrite => "read_write",
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
59pub struct Vfs {
60 root: PathBuf,
61 cwd: Arc<str>,
62 ancestors: Arc<[FileIdentity]>,
63 mode: VfsMode,
64 dir: Option<Arc<Dir>>,
66 _owned: Option<Arc<TempDir>>,
67 quota: Option<Arc<DiskQuota>>,
70 access: Access,
71 volume: Option<Arc<str>>,
73 mounts: Arc<[Mount]>,
74}
75
76#[derive(Debug, Clone)]
78pub struct Mount {
79 guest: Arc<str>,
81 rel: PathBuf,
83 volume: Arc<str>,
84 dir: Arc<Dir>,
85 host: Arc<Path>,
87 access: Access,
88 quota: Option<Arc<DiskQuota>>,
89 placeholders: Arc<[Placeholder]>,
94 ancestors: Arc<[FileIdentity]>,
95}
96
97impl Mount {
98 pub fn guest_path(&self) -> &str {
99 &self.guest
100 }
101
102 pub fn volume(&self) -> &str {
103 &self.volume
104 }
105
106 pub fn access(&self) -> Access {
107 self.access
108 }
109
110 pub fn quota(&self) -> Option<&Arc<DiskQuota>> {
111 self.quota.as_ref()
112 }
113
114 pub(crate) fn rel(&self) -> &Path {
115 &self.rel
116 }
117
118 pub fn dir(&self) -> &Arc<Dir> {
119 &self.dir
120 }
121
122 pub fn host(&self) -> &Path {
123 &self.host
124 }
125
126 pub(crate) fn placeholders(&self) -> &[Placeholder] {
127 &self.placeholders
128 }
129}
130
131#[derive(Debug, Clone)]
134pub(crate) struct Placeholder {
135 pub(crate) identity: FileIdentity,
136 pub(crate) parent: PathBuf,
138 pub(crate) name: OsString,
139}
140
141pub struct MountSpec {
143 pub guest_path: String,
146 pub host: PathBuf,
147 pub volume: String,
148 pub access: Access,
149 pub quota: Option<Arc<DiskQuota>>,
151}
152
153#[derive(Debug, Clone)]
156pub struct Placement {
157 access: Access,
158 quota: Option<Arc<DiskQuota>>,
159 mount: Option<Arc<str>>,
161 nested: Arc<[Mount]>,
164 host: Arc<Path>,
166 volume: Option<Arc<str>>,
167 ancestors: Arc<[FileIdentity]>,
168 protected_roots: Arc<[FileIdentity]>,
169 protected_ancestors: Arc<[FileIdentity]>,
170}
171
172impl Placement {
173 pub fn access(&self) -> Access {
174 self.access
175 }
176
177 pub fn quota(&self) -> Option<&Arc<DiskQuota>> {
178 self.quota.as_ref()
179 }
180
181 pub fn mount_point(&self) -> &str {
183 self.mount.as_deref().unwrap_or("/")
184 }
185
186 pub(crate) fn ancestors(&self) -> &[FileIdentity] {
187 &self.ancestors
188 }
189
190 pub(crate) fn protects(&self, identity: FileIdentity, recursive: bool) -> bool {
191 self.protected_roots.contains(&identity)
192 || (recursive && self.protected_ancestors.contains(&identity))
193 }
194
195 pub(crate) fn protects_descendants(&self, identity: FileIdentity) -> bool {
196 self.protected_ancestors.contains(&identity)
197 }
198
199 pub(crate) fn nested(&self) -> &[Mount] {
200 &self.nested
201 }
202
203 pub(crate) fn host(&self) -> &Path {
207 &self.host
208 }
209
210 pub fn same_volume(&self, other: &Self) -> bool {
213 match (&self.volume, &other.volume) {
214 (Some(left), Some(right)) => left == right,
215 (None, None) => self.mount == other.mount,
216 _ => false,
217 }
218 }
219}
220
221#[derive(Debug)]
223pub enum MountError {
224 RootDisabled,
226 BadPath(String),
228 AtRoot,
230 ProtectedPath(String),
232 Nested {
234 outer: String,
235 inner: String,
236 },
237 DuplicateVolume {
239 volume: String,
240 at: String,
241 },
242 TooMany,
243 MountPointUnavailable {
246 path: String,
247 reason: &'static str,
248 },
249 RootIo(io::Error),
251 Io(io::Error),
254}
255
256impl fmt::Display for MountError {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 match self {
259 Self::RootDisabled => {
260 f.write_str("mounts need a filesystem root, and vfs mode `none` has none")
261 }
262 Self::BadPath(path) => write!(
263 f,
264 "mount path `{path}` must be absolute and normalized, made of ASCII letters, \
265 digits, `.`, `_` and `-`, such as `/memory`"
266 ),
267 Self::AtRoot => f.write_str("a volume cannot be mounted at `/`; that is the root"),
268 Self::ProtectedPath(path) => {
269 write!(f, "mount path `{path}` names Git metadata")
270 }
271 Self::Nested { outer, inner } => {
272 write!(
273 f,
274 "mount `{inner}` is inside mount `{outer}`; mounts may not nest"
275 )
276 }
277 Self::DuplicateVolume { volume, at } => {
278 write!(f, "volume `{volume}` is already mounted at `{at}`")
279 }
280 Self::TooMany => write!(f, "more than {MAX_MOUNTS} mounts"),
281 Self::MountPointUnavailable { path, reason } => {
282 write!(f, "mount point `{path}` {reason}")
283 }
284 Self::RootIo(err) => write!(f, "the mount point could not be prepared: {err}"),
285 Self::Io(err) => write!(f, "{err}"),
286 }
287 }
288}
289
290impl std::error::Error for MountError {}
291
292pub const MAX_MOUNTS: usize = 16;
296
297pub const TEMPDIR_PREFIX: &str = "submilli-vfs-";
300
301impl Vfs {
302 pub fn none() -> Self {
305 Self {
306 root: PathBuf::new(),
307 mode: VfsMode::None,
308 dir: None,
309 _owned: None,
310 quota: None,
311 access: Access::ReadWrite,
312 volume: None,
313 mounts: Arc::from([]),
314 cwd: Arc::from("/"),
315 ancestors: Arc::from([]),
316 }
317 }
318
319 pub fn external_with_mode(root: PathBuf, mode: VfsMode) -> io::Result<Self> {
322 let meta = std::fs::metadata(&root)
323 .map_err(|e| io::Error::new(e.kind(), format!("VFS root {}: {e}", root.display())))?;
324 if !meta.is_dir() {
325 return Err(io::Error::new(
326 io::ErrorKind::NotADirectory,
327 format!("VFS root {} is not a directory", root.display()),
328 ));
329 }
330 let dir = open_root(&root)?;
331 let identity = dir_identity(&dir, Path::new("."), &dir.dir_metadata()?)?;
332 Ok(Self {
333 root,
334 mode,
335 dir: Some(dir),
336 _owned: None,
337 quota: None,
338 access: Access::ReadWrite,
339 volume: None,
340 mounts: Arc::from([]),
341 cwd: Arc::from("/"),
342 ancestors: Arc::from([identity]),
343 })
344 }
345
346 pub fn external(root: PathBuf) -> io::Result<Self> {
348 Self::external_with_mode(root, VfsMode::Named)
349 }
350
351 pub fn tempdir() -> io::Result<Self> {
352 let td = tempfile::Builder::new().prefix(TEMPDIR_PREFIX).tempdir()?;
353 Self::from_owned(td)
354 }
355
356 pub fn tempdir_in(parent: &Path) -> io::Result<Self> {
360 let td = tempfile::Builder::new()
361 .prefix(TEMPDIR_PREFIX)
362 .tempdir_in(parent)?;
363 Self::from_owned(td)
364 }
365
366 fn from_owned(td: TempDir) -> io::Result<Self> {
367 let root = td.path().to_path_buf();
368 let dir = open_root(&root)?;
369 let identity = dir_identity(&dir, Path::new("."), &dir.dir_metadata()?)?;
370 Ok(Self {
371 root,
372 mode: VfsMode::Ephemeral,
373 dir: Some(dir),
374 _owned: Some(Arc::new(td)),
375 quota: None,
376 access: Access::ReadWrite,
377 volume: None,
378 mounts: Arc::from([]),
379 cwd: Arc::from("/"),
380 ancestors: Arc::from([identity]),
381 })
382 }
383
384 pub fn root(&self) -> &Path {
388 &self.root
389 }
390
391 pub fn dir(&self) -> Option<&Arc<Dir>> {
397 self.dir.as_ref()
398 }
399
400 pub fn mode(&self) -> VfsMode {
401 self.mode
402 }
403
404 pub fn is_disposable(&self) -> bool {
406 self._owned.is_some()
407 }
408
409 pub fn with_size_limit(self, limit: u64) -> Self {
412 let measured = self.measure_usage().ok();
413 self.with_measured_limit(limit, measured)
414 }
415
416 pub fn with_measured_limit(mut self, limit: u64, used: Option<u64>) -> Self {
422 let quota = match used {
423 Some(used) => DiskQuota::new(limit, used),
424 None => DiskQuota::unmeasured(limit),
425 };
426 self.quota = Some(Arc::new(quota));
427 self
428 }
429
430 pub fn with_shared_quota(mut self, quota: Arc<DiskQuota>) -> Self {
433 self.quota = Some(quota);
434 self
435 }
436
437 pub fn quota(&self) -> Option<&Arc<DiskQuota>> {
439 self.quota.as_ref()
440 }
441
442 pub fn with_access(mut self, access: Access) -> Self {
444 self.access = access;
445 self
446 }
447
448 pub fn access(&self) -> Access {
449 self.access
450 }
451
452 pub fn with_volume_name(mut self, volume: &str) -> Self {
454 self.volume = Some(Arc::from(volume));
455 self
456 }
457
458 pub fn volume(&self) -> Option<&str> {
459 self.volume.as_deref()
460 }
461
462 pub fn mounts(&self) -> &[Mount] {
463 &self.mounts
464 }
465
466 pub fn with_mount(self, spec: MountSpec) -> Result<Self, MountError> {
472 self.with_mount_subpath(spec, None)
473 }
474
475 pub fn with_mount_subpath(
476 mut self,
477 spec: MountSpec,
478 sub_path: Option<&str>,
479 ) -> Result<Self, MountError> {
480 let root = self.dir.as_ref().ok_or(MountError::RootDisabled)?;
481 if self.mounts.len() >= MAX_MOUNTS {
482 return Err(MountError::TooMany);
483 }
484 let rel = mount_rel(&spec.guest_path)?;
485 if crate::runtime::fs::protected_metadata(&rel) {
486 return Err(MountError::ProtectedPath(spec.guest_path));
487 }
488 for mount in self.mounts.iter() {
491 let (outer, inner) = if starts_with_folded(&rel, &mount.rel) {
492 (mount.guest.to_string(), spec.guest_path.clone())
493 } else if starts_with_folded(&mount.rel, &rel) {
494 (spec.guest_path.clone(), mount.guest.to_string())
495 } else {
496 continue;
497 };
498 return Err(MountError::Nested { outer, inner });
499 }
500 let placeholders = prepare_mount_point(root, &rel, self.access, &spec.guest_path)?;
501 let (dir, ancestors) = select_directory(
502 open_volume(&spec.host).map_err(MountError::Io)?,
503 sub_path,
504 spec.access,
505 )
506 .map_err(MountError::Io)?;
507 let mount = Mount {
508 guest: Arc::from(spec.guest_path.as_str()),
509 rel,
510 volume: Arc::from(spec.volume.as_str()),
511 dir,
512 host: Arc::from(
513 sub_path.map_or_else(|| spec.host.clone(), |path| spec.host.join(path)),
514 ),
515 access: spec.access,
516 quota: spec.quota,
517 placeholders: Arc::from(placeholders),
518 ancestors: Arc::from(ancestors),
519 };
520 let mut mounts = self.mounts.to_vec();
521 mounts.push(mount);
522 self.mounts = Arc::from(mounts);
523 Ok(self)
524 }
525
526 pub fn with_mount_quota(mut self, guest_path: &str, quota: Arc<DiskQuota>) -> Self {
530 let mounts: Vec<Mount> = self
531 .mounts
532 .iter()
533 .cloned()
534 .map(|mut mount| {
535 if &*mount.guest == guest_path {
536 mount.quota = Some(Arc::clone("a));
537 }
538 mount
539 })
540 .collect();
541 self.mounts = Arc::from(mounts);
542 self
543 }
544
545 pub fn locate(&self, rel: &Path) -> Option<(Arc<Dir>, PathBuf, Placement)> {
553 let root = self.dir.as_ref()?;
554 let (protected_roots, protected_ancestors) = self.protected_directories();
555 for mount in self.mounts.iter() {
556 if let Ok(rest) = rel.strip_prefix(&mount.rel) {
557 let rest = if rest.as_os_str().is_empty() {
558 PathBuf::from(".")
559 } else {
560 rest.to_path_buf()
561 };
562 let placement = Placement {
563 access: mount.access,
564 quota: mount.quota.clone(),
565 mount: Some(Arc::clone(&mount.guest)),
566 nested: Arc::from([]),
567 host: Arc::clone(&mount.host),
568 volume: Some(Arc::clone(&mount.volume)),
569 ancestors: Arc::clone(&mount.ancestors),
570 protected_roots,
571 protected_ancestors,
572 };
573 return Some((Arc::clone(&mount.dir), rest, placement));
574 }
575 }
576 let placement = Placement {
577 access: self.access,
578 quota: self.quota.clone(),
579 mount: None,
580 nested: Arc::clone(&self.mounts),
581 host: Arc::from(self.root.as_path()),
582 volume: self.volume.clone(),
583 ancestors: Arc::clone(&self.ancestors),
584 protected_roots,
585 protected_ancestors,
586 };
587 Some((Arc::clone(root), rel.to_path_buf(), placement))
588 }
589
590 fn protected_directories(&self) -> (Arc<[FileIdentity]>, Arc<[FileIdentity]>) {
591 let mut roots = Vec::new();
592 let mut ancestors = self
593 .ancestors
594 .split_last()
595 .map_or(&[][..], |(_, parents)| parents)
596 .to_vec();
597 if let Some(identity) = self.ancestors.last() {
598 roots.push(*identity);
599 if !self.mounts.is_empty() {
600 ancestors.push(*identity);
601 }
602 }
603 for mount in self.mounts.iter() {
604 if let Some(identity) = mount.ancestors.last() {
605 roots.push(*identity);
606 }
607 if let Some((_, parents)) = mount.ancestors.split_last() {
608 ancestors.extend_from_slice(parents);
609 }
610 for placeholder in mount.placeholders.iter() {
612 ancestors.push(placeholder.identity);
613 }
614 if let Some(placeholder) = mount.placeholders.last() {
615 roots.push(placeholder.identity);
616 }
617 }
618 (Arc::from(roots), Arc::from(ancestors))
619 }
620
621 pub fn cwd(&self) -> &str {
623 &self.cwd
624 }
625
626 pub fn with_cwd(self, cwd: &str) -> Result<Self, crate::runtime::fs::ContainError> {
627 use crate::runtime::fs::{ContainError, resolve_content};
628 let vfs = self.with_cwd_path(cwd)?;
629 let target = resolve_content(&vfs, "/", cwd)?;
630 match target.open_dir() {
631 Ok(_) => {}
632 Err(ContainError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
633 target.create_dir_all()?;
634 target.open_dir()?;
635 }
636 Err(error) => return Err(error),
637 }
638 Ok(vfs)
639 }
640
641 pub fn with_cwd_path(mut self, cwd: &str) -> Result<Self, crate::runtime::fs::ContainError> {
643 use crate::runtime::fs::{ContainError, guest_normalize};
644 if !cwd.starts_with('/') || cwd.len() > 4096 || guest_normalize("/", cwd)? != cwd {
645 return Err(ContainError::Io(io::Error::new(
646 io::ErrorKind::InvalidInput,
647 "cwd must be an absolute normalized guest path",
648 )));
649 }
650 self.cwd = Arc::from(cwd);
651 Ok(self)
652 }
653
654 pub fn with_subpath(mut self, sub_path: Option<&str>) -> io::Result<Self> {
656 let root = self
657 .dir
658 .clone()
659 .ok_or_else(|| io::Error::other("filesystem disabled"))?;
660 let (dir, ancestors) = select_directory(root, sub_path, self.access)?;
661 self.dir = Some(dir);
662 if let Some(path) = sub_path {
663 self.root.push(path);
664 }
665 self.ancestors = Arc::from(ancestors);
666 Ok(self)
667 }
668
669 pub fn measure_usage(&self) -> io::Result<u64> {
671 match &self.dir {
672 Some(root) => measure_dir(root),
673 None => Ok(0),
674 }
675 }
676}
677
678pub fn measure_host_dir(root: &Path) -> io::Result<u64> {
682 measure_dir(&*open_volume(root)?)
683}
684
685pub fn measure_host_dir_skipping_vanished(root: &Path) -> io::Result<u64> {
688 let mut total: u64 = 0;
689 let root = open_volume(root)?;
690 for_each_regular_file(&root, MAX_MEASURED_ENTRIES, true, |_, bytes| {
691 total = total.saturating_add(bytes);
692 })?;
693 Ok(total)
694}
695
696pub fn measure_host_subdir_skipping_vanished(volume: &Path, sub: &Path) -> io::Result<u64> {
699 let Some(root) = open_host_subdir(volume, sub)? else {
700 return Ok(0);
701 };
702 let mut total: u64 = 0;
703 for_each_regular_file(&root, MAX_MEASURED_ENTRIES, true, |_, bytes| {
704 total = total.saturating_add(bytes);
705 })?;
706 Ok(total)
707}
708
709pub fn open_host_subdir(volume: &Path, sub: &Path) -> io::Result<Option<Arc<Dir>>> {
713 let mut dir = open_volume(volume)?;
714 for component in sub.components() {
715 let Component::Normal(name) = component else {
716 return Err(io::Error::new(
717 io::ErrorKind::InvalidInput,
718 "a sub-path must be a normalized relative path",
719 ));
720 };
721 dir = match dir.open_dir_nofollow(name) {
722 Ok(child) => Arc::new(child),
723 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
724 Err(error) => {
725 return Err(io::Error::new(
726 error.kind(),
727 format!("{}: {error}", sub.display()),
728 ));
729 }
730 };
731 }
732 Ok(Some(dir))
733}
734
735pub fn measure_dir(root: &Dir) -> io::Result<u64> {
737 let mut total: u64 = 0;
738 for_each_regular_file(root, MAX_MEASURED_ENTRIES, false, |_, bytes| {
739 total = total.saturating_add(bytes);
740 })?;
741 Ok(total)
742}
743
744pub fn regular_files(root: &Dir, max_entries: usize) -> io::Result<Vec<(FileIdentity, u64)>> {
747 let mut files = Vec::new();
748 for_each_regular_file(root, max_entries, false, |file, bytes| {
749 files.push((file, bytes));
750 })?;
751 Ok(files)
752}
753
754fn for_each_regular_file(
761 root: &Dir,
762 max_entries: usize,
763 skip_vanished: bool,
764 mut visit: impl FnMut(FileIdentity, u64),
765) -> io::Result<()> {
766 let mut entries: usize = 0;
767 let mut open: Vec<cap_std::fs::ReadDir> = vec![root.entries()?];
768 while let Some(dir) = open.last_mut() {
769 let Some(entry) = dir.next() else {
770 open.pop();
771 continue;
772 };
773 let entry = entry?;
774 entries += 1;
775 if entries > max_entries {
776 return Err(io::Error::new(
777 io::ErrorKind::InvalidData,
778 format!("more than {max_entries} entries"),
779 ));
780 }
781 let Some(kind) = vanished(entry.file_type(), skip_vanished)? else {
782 continue;
783 };
784 if kind.is_dir() {
785 if open.len() > MAX_MEASURED_DEPTH {
786 return Err(io::Error::new(
787 io::ErrorKind::InvalidData,
788 format!("directories nested more than {MAX_MEASURED_DEPTH} deep"),
789 ));
790 }
791 let Some(child) = vanished(
792 entry.open_dir().and_then(|dir| dir.entries()),
793 skip_vanished,
794 )?
795 else {
796 continue;
797 };
798 open.push(child);
799 } else if kind.is_file() {
800 let Some(metadata) = vanished(
803 cap_fs_ext::DirEntryExt::full_metadata(&entry),
804 skip_vanished,
805 )?
806 else {
807 continue;
808 };
809 visit(FileIdentity::of(&metadata)?, metadata.len());
810 }
811 }
812 Ok(())
813}
814
815fn vanished<T>(result: io::Result<T>, skip: bool) -> io::Result<Option<T>> {
817 match result {
818 Ok(value) => Ok(Some(value)),
819 Err(error) if skip && error.kind() == io::ErrorKind::NotFound => Ok(None),
820 Err(error) => Err(error),
821 }
822}
823
824#[derive(Debug)]
826pub enum CopyDirError {
827 Io(io::Error),
828 OverCap,
830 TooMany,
832}
833
834impl From<io::Error> for CopyDirError {
835 fn from(error: io::Error) -> Self {
836 Self::Io(error)
837 }
838}
839
840pub fn copy_host_dir(
849 from: &Path,
850 to: &Path,
851 cap: u64,
852 bytes: &mut u64,
853) -> Result<(), CopyDirError> {
854 let source = open_volume(from)?;
855 std::fs::create_dir_all(to)?;
856 let target = Dir::open_ambient_dir(to, ambient_authority())?;
857 let mut copy = TreeCopy {
858 cap,
859 bytes,
860 entries: 0,
861 rel: PathBuf::new(),
862 };
863 copy.dir(&source, &target, 0)
864}
865
866pub fn copy_host_subdir(
871 volume: &Path,
872 sub: &Path,
873 to_root: &Path,
874 cap: u64,
875 bytes: &mut u64,
876) -> Result<(), CopyDirError> {
877 std::fs::create_dir_all(to_root)?;
878 let Some(source) = open_host_subdir(volume, sub)? else {
879 return Ok(());
880 };
881 let mut target = Dir::open_ambient_dir(to_root, ambient_authority())?;
882 for component in sub.components() {
883 target = match target.create_dir(component) {
884 Ok(()) => target.open_dir_nofollow(component)?,
885 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
886 target.open_dir_nofollow(component)?
887 }
888 Err(error) => return Err(error.into()),
889 };
890 }
891 let mut copy = TreeCopy {
892 cap,
893 bytes,
894 entries: 0,
895 rel: sub.to_path_buf(),
896 };
897 copy.dir(&source, &target, 0)
898}
899
900#[derive(Clone, Copy)]
901enum EntryKind {
902 Dir,
903 File,
904 Link,
905 Other,
906}
907
908impl EntryKind {
909 fn of(file_type: cap_std::fs::FileType) -> Self {
910 if file_type.is_dir() {
911 Self::Dir
912 } else if file_type.is_file() {
913 Self::File
914 } else if file_type.is_symlink() {
915 Self::Link
916 } else {
917 Self::Other
918 }
919 }
920}
921
922struct TreeCopy<'a> {
923 cap: u64,
924 bytes: &'a mut u64,
925 entries: usize,
926 rel: PathBuf,
928}
929
930impl TreeCopy<'_> {
931 fn at(&self, name: &OsStr, error: io::Error) -> CopyDirError {
933 let path = self.rel.join(name);
934 let shown = if path.as_os_str().is_empty() {
935 Path::new(".")
936 } else {
937 &path
938 };
939 io::Error::new(error.kind(), format!("{}: {error}", shown.display())).into()
940 }
941
942 fn dir(&mut self, source: &Dir, target: &Dir, depth: usize) -> Result<(), CopyDirError> {
943 let entries = source
944 .entries()
945 .map_err(|error| self.at(OsStr::new(""), error))?;
946 for entry in entries {
947 let entry = entry.map_err(|error| self.at(OsStr::new(""), error))?;
948 self.entries += 1;
949 if self.entries > MAX_MEASURED_ENTRIES {
950 return Err(CopyDirError::TooMany);
951 }
952 let Some(file_type) = vanished(entry.file_type(), true)? else {
953 continue;
954 };
955 let kind = EntryKind::of(file_type);
956 self.entry(source, target, &entry.file_name(), kind, depth)?;
957 }
958 Ok(())
959 }
960
961 fn entry(
964 &mut self,
965 source: &Dir,
966 target: &Dir,
967 name: &OsStr,
968 kind: EntryKind,
969 depth: usize,
970 ) -> Result<(), CopyDirError> {
971 match kind {
972 EntryKind::Dir => self.subdir(source, target, name, depth),
973 EntryKind::File => self.file(source, target, name, false),
974 EntryKind::Link => self.link(source, target, name, false),
975 EntryKind::Other => Ok(()),
976 }
977 }
978
979 fn subdir(
980 &mut self,
981 source: &Dir,
982 target: &Dir,
983 name: &OsStr,
984 depth: usize,
985 ) -> Result<(), CopyDirError> {
986 if depth >= MAX_MEASURED_DEPTH {
987 return Err(CopyDirError::TooMany);
988 }
989 let child = match source.open_dir_nofollow(name) {
990 Ok(child) => child,
991 Err(error) => return self.changed(source, target, name, EntryKind::Dir, error, false),
992 };
993 match target.create_dir(name) {
994 Ok(()) => {}
995 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => return Ok(()),
997 Err(error) => return Err(self.at(name, error)),
998 }
999 let copied = target
1000 .open_dir(name)
1001 .map_err(|error| self.at(name, error))?;
1002 self.rel.push(name);
1003 let result = self.dir(&child, &copied, depth + 1);
1004 self.rel.pop();
1005 result
1006 }
1007
1008 fn file(
1010 &mut self,
1011 source: &Dir,
1012 target: &Dir,
1013 name: &OsStr,
1014 reclassified: bool,
1015 ) -> Result<(), CopyDirError> {
1016 let mut options = cap_std::fs::OpenOptions::new();
1017 options.read(true).follow(FollowSymlinks::No);
1018 #[cfg(unix)]
1020 cap_std::fs::OpenOptionsExt::custom_flags(&mut options, libc::O_NONBLOCK | libc::O_NOCTTY);
1021 let mut input = match source.open_with(name, &options) {
1022 Ok(file) => file,
1023 Err(error) => {
1024 return self.changed(source, target, name, EntryKind::File, error, reclassified);
1025 }
1026 };
1027 let metadata = input.metadata().map_err(|error| self.at(name, error))?;
1028 if !metadata.is_file() {
1029 return Ok(());
1030 }
1031 let mut output = match target.open_with(
1032 name,
1033 cap_std::fs::OpenOptions::new().write(true).create_new(true),
1034 ) {
1035 Ok(file) => file,
1036 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => return Ok(()),
1038 Err(error) => return Err(self.at(name, error)),
1039 };
1040 let room = self.cap.saturating_sub(*self.bytes).saturating_add(1);
1042 let copied = io::copy(&mut input.by_ref().take(room), &mut output)
1043 .map_err(|error| self.at(name, error))?;
1044 *self.bytes = self.bytes.saturating_add(copied);
1045 if *self.bytes > self.cap {
1046 return Err(CopyDirError::OverCap);
1047 }
1048 output
1049 .set_permissions(metadata.permissions())
1050 .map_err(|error| self.at(name, error))?;
1051 Ok(())
1052 }
1053
1054 fn link(
1055 &mut self,
1056 source: &Dir,
1057 target: &Dir,
1058 name: &OsStr,
1059 reclassified: bool,
1060 ) -> Result<(), CopyDirError> {
1061 let to = match source.read_link_contents(name) {
1064 Ok(to) => to,
1065 Err(error) => {
1066 return self.changed(source, target, name, EntryKind::Link, error, reclassified);
1067 }
1068 };
1069 #[cfg(not(windows))]
1070 match target.symlink_contents(to, name) {
1071 Ok(()) => {}
1072 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1074 Err(error) => return Err(self.at(name, error)),
1075 }
1076 #[cfg(windows)]
1077 let _ = to;
1078 Ok(())
1079 }
1080
1081 fn changed(
1088 &mut self,
1089 source: &Dir,
1090 target: &Dir,
1091 name: &OsStr,
1092 listed: EntryKind,
1093 error: io::Error,
1094 reclassified: bool,
1095 ) -> Result<(), CopyDirError> {
1096 if error.kind() == io::ErrorKind::NotFound {
1097 return Ok(());
1098 }
1099 if reclassified {
1100 return if wrong_kind(&error) {
1101 Ok(())
1102 } else {
1103 Err(self.at(name, error))
1104 };
1105 }
1106 let now = match source.symlink_metadata(name) {
1107 Ok(meta) => EntryKind::of(meta.file_type()),
1108 Err(gone) if gone.kind() == io::ErrorKind::NotFound => return Ok(()),
1109 Err(_) => return Err(self.at(name, error)),
1110 };
1111 match (listed, now) {
1112 (EntryKind::Dir | EntryKind::Link, EntryKind::File) => {
1113 self.file(source, target, name, true)
1114 }
1115 (EntryKind::Dir | EntryKind::File, EntryKind::Link) => {
1116 self.link(source, target, name, true)
1117 }
1118 (EntryKind::File | EntryKind::Link, EntryKind::Dir) | (_, EntryKind::Other) => Ok(()),
1120 _ if wrong_kind(&error) => Ok(()),
1123 _ => Err(self.at(name, error)),
1124 }
1125 }
1126}
1127
1128fn wrong_kind(error: &io::Error) -> bool {
1131 #[cfg(unix)]
1132 {
1133 matches!(
1134 error.raw_os_error(),
1135 Some(libc::ENOTDIR | libc::ELOOP | libc::EINVAL)
1136 )
1137 }
1138 #[cfg(not(unix))]
1139 {
1140 let _ = error;
1141 false
1142 }
1143}
1144
1145const MAX_MEASURED_ENTRIES: usize = 1_000_000;
1148
1149pub const MAX_MEASURED_DEPTH: usize = 64;
1152
1153fn mount_rel(guest: &str) -> Result<PathBuf, MountError> {
1159 let bad = || MountError::BadPath(guest.to_string());
1160 let Some(rest) = guest.strip_prefix('/') else {
1161 return Err(bad());
1162 };
1163 if rest.is_empty() {
1164 return Err(MountError::AtRoot);
1165 }
1166 let mut rel = PathBuf::new();
1167 for part in rest.split('/') {
1168 let plain = part
1169 .chars()
1170 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'));
1171 if part.is_empty() || part.ends_with('.') || !plain {
1174 return Err(bad());
1175 }
1176 rel.push(part);
1177 }
1178 Ok(rel)
1179}
1180
1181fn starts_with_folded(path: &Path, prefix: &Path) -> bool {
1184 let mut parts = path.components();
1185 prefix.components().all(|want| {
1186 parts.next().is_some_and(|have| {
1187 have.as_os_str()
1188 .as_encoded_bytes()
1189 .eq_ignore_ascii_case(want.as_os_str().as_encoded_bytes())
1190 })
1191 })
1192}
1193
1194fn prepare_mount_point(
1200 root: &Dir,
1201 rel: &Path,
1202 access: Access,
1203 guest: &str,
1204) -> Result<Vec<Placeholder>, MountError> {
1205 let unavailable = |reason| MountError::MountPointUnavailable {
1206 path: guest.to_string(),
1207 reason,
1208 };
1209 let mut prefix = PathBuf::new();
1210 for part in rel.components() {
1211 prefix.push(part);
1212 match root.symlink_metadata(&prefix) {
1213 Ok(meta) if meta.is_dir() => {
1214 match spelling_of(root, &prefix).map_err(MountError::RootIo)? {
1215 Spelling::Exact => {}
1216 Spelling::Variant => {
1217 return Err(unavailable(
1218 "exists in the root under a different letter case; rename it to \
1219 match",
1220 ));
1221 }
1222 Spelling::Unknown => {
1223 return Err(unavailable(
1224 "sits beside too many entries to check its spelling; move some \
1225 of them into a subdirectory",
1226 ));
1227 }
1228 }
1229 }
1230 Ok(_) => return Err(unavailable("exists in the root and is not a directory")),
1231 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1232 if access == Access::ReadOnly {
1233 return Err(unavailable(
1234 "does not exist, and a read-only root cannot create it",
1235 ));
1236 }
1237 match root.create_dir(&prefix) {
1238 Ok(()) => {}
1239 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
1241 Err(err) => return Err(MountError::RootIo(err)),
1242 }
1243 }
1244 Err(err) => return Err(MountError::RootIo(err)),
1245 }
1246 }
1247 match root.symlink_metadata(rel) {
1249 Ok(meta) if meta.is_dir() => {}
1250 Ok(_) => return Err(unavailable("exists in the root and is not a directory")),
1251 Err(err) => return Err(MountError::RootIo(err)),
1252 }
1253 placeholders_on(root, rel).map_err(MountError::RootIo)
1254}
1255
1256fn placeholders_on(root: &Dir, rel: &Path) -> io::Result<Vec<Placeholder>> {
1261 let mut placeholders = Vec::new();
1262 let mut prefix = PathBuf::new();
1263 for part in rel.components() {
1264 prefix.push(part);
1265 let meta = root.symlink_metadata(&prefix)?;
1266 placeholders.push(Placeholder {
1267 identity: dir_identity(root, &prefix, &meta)?,
1268 parent: prefix.parent().map(Path::to_path_buf).unwrap_or_default(),
1269 name: part.as_os_str().to_os_string(),
1270 });
1271 }
1272 Ok(placeholders)
1273}
1274
1275enum Spelling {
1277 Exact,
1278 Variant,
1280 Unknown,
1282}
1283
1284fn spelling_of(root: &Dir, path: &Path) -> io::Result<Spelling> {
1295 let Some(name) = path.file_name() else {
1296 return Ok(Spelling::Exact);
1297 };
1298 let flipped: String = name
1299 .to_string_lossy()
1300 .chars()
1301 .map(|c| {
1302 if c.is_ascii_lowercase() {
1303 c.to_ascii_uppercase()
1304 } else {
1305 c.to_ascii_lowercase()
1306 }
1307 })
1308 .collect();
1309 if flipped.as_str() == name {
1310 return Ok(Spelling::Exact);
1311 }
1312 let parent = path.parent().unwrap_or(Path::new(""));
1313 let other = match root.symlink_metadata(parent.join(&flipped)) {
1314 Ok(meta) => meta,
1315 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Spelling::Exact),
1316 Err(err) => return Err(err),
1317 };
1318 let this = root.symlink_metadata(path)?;
1319 if let (Ok(this), Ok(other)) = (FileIdentity::of(&this), FileIdentity::of(&other))
1320 && this != other
1321 {
1322 return Ok(Spelling::Exact);
1323 }
1324 let entries = if parent.as_os_str().is_empty() {
1325 root.entries()?
1326 } else {
1327 root.read_dir(parent)?
1328 };
1329 for (index, entry) in entries.enumerate() {
1330 if index >= MAX_SPELLING_SCAN {
1331 return Ok(Spelling::Unknown);
1332 }
1333 if entry?.file_name() == name {
1334 return Ok(Spelling::Exact);
1335 }
1336 }
1337 Ok(Spelling::Variant)
1338}
1339
1340const MAX_SPELLING_SCAN: usize = 100_000;
1343
1344fn open_volume(host: &Path) -> io::Result<Arc<Dir>> {
1345 let meta = std::fs::metadata(host)?;
1346 if !meta.is_dir() {
1347 return Err(io::Error::new(
1348 io::ErrorKind::NotADirectory,
1349 "volume is not a directory",
1350 ));
1351 }
1352 Dir::open_ambient_dir(host, ambient_authority()).map(Arc::new)
1353}
1354
1355fn select_directory(
1356 mut dir: Arc<Dir>,
1357 sub_path: Option<&str>,
1358 access: Access,
1359) -> io::Result<(Arc<Dir>, Vec<FileIdentity>)> {
1360 let mut ancestors = vec![dir_identity(&dir, Path::new("."), &dir.dir_metadata()?)?];
1361 let Some(path) = sub_path else {
1362 return Ok((dir, ancestors));
1363 };
1364 if path.is_empty()
1365 || path.len() > 4096
1366 || path.split('/').any(|part| {
1367 part.is_empty() || part == "." || part == ".." || part.contains(['\\', '\0'])
1368 })
1369 {
1370 return Err(io::Error::new(
1371 io::ErrorKind::InvalidInput,
1372 "subPath must be a normalized relative path",
1373 ));
1374 }
1375 for component in path.split('/') {
1376 let child = match dir.open_dir_nofollow(component) {
1377 Ok(child) => child,
1378 Err(error)
1379 if error.kind() == io::ErrorKind::NotFound && access == Access::ReadWrite =>
1380 {
1381 match dir.create_dir(component) {
1382 Ok(()) => {}
1383 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1384 Err(error) => return Err(error),
1385 }
1386 dir.open_dir_nofollow(component)?
1387 }
1388 Err(error) => return Err(error),
1389 };
1390 ancestors.push(dir_identity(
1391 &child,
1392 Path::new("."),
1393 &child.dir_metadata()?,
1394 )?);
1395 dir = Arc::new(child);
1396 }
1397 Ok((dir, ancestors))
1398}
1399
1400fn open_root(root: &Path) -> io::Result<Arc<Dir>> {
1401 Dir::open_ambient_dir(root, ambient_authority())
1402 .map(Arc::new)
1403 .map_err(|e| io::Error::new(e.kind(), format!("VFS root {}: {e}", root.display())))
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408 use super::*;
1409
1410 #[test]
1411 fn subpath_creation_and_readonly_cwd() {
1412 let volume = tempfile::tempdir().unwrap();
1413 let vfs = Vfs::external(volume.path().into())
1414 .unwrap()
1415 .with_subpath(Some("users/ada"))
1416 .unwrap();
1417 assert!(volume.path().join("users/ada").is_dir());
1418 let vfs = vfs.with_cwd("/notes/drafts").unwrap();
1419 assert_eq!(vfs.cwd(), "/notes/drafts");
1420 assert!(volume.path().join("users/ada/notes/drafts").is_dir());
1421 let readonly = vfs.with_access(Access::ReadOnly);
1422 assert!(readonly.clone().with_cwd("/notes").is_ok());
1423 assert!(readonly.with_cwd("/missing").is_err());
1424 assert!(
1425 Vfs::external(volume.path().into())
1426 .unwrap()
1427 .with_access(Access::ReadOnly)
1428 .with_subpath(Some("absent"))
1429 .is_err()
1430 );
1431 }
1432
1433 #[cfg(unix)]
1434 #[test]
1435 fn subpath_selection_rejects_symlinks() {
1436 let volume = tempfile::tempdir().unwrap();
1437 std::fs::create_dir_all(volume.path().join("users/ada")).unwrap();
1438 std::os::unix::fs::symlink("ada", volume.path().join("users/alias")).unwrap();
1439 let vfs = Vfs::external(volume.path().into()).unwrap();
1440 assert!(vfs.with_subpath(Some("users/alias")).is_err());
1441 }
1442
1443 #[test]
1444 fn tempdir_is_deleted_on_drop() {
1445 let vfs = Vfs::tempdir().expect("tempdir");
1446 let path = vfs.root().to_path_buf();
1447 assert!(path.is_dir());
1448 drop(vfs);
1449 assert!(!path.exists(), "tempdir should be deleted on drop");
1450 }
1451
1452 #[test]
1453 fn external_does_not_delete_directory() {
1454 let outer = tempfile::tempdir().expect("outer tempdir");
1455 let path = outer.path().to_path_buf();
1456 let vfs = Vfs::external(path.clone()).expect("external");
1457 drop(vfs);
1458 assert!(path.is_dir(), "external path must survive Vfs drop");
1459 }
1460
1461 #[test]
1462 fn external_rejects_missing_path() {
1463 let outer = tempfile::tempdir().expect("outer tempdir");
1464 let missing = outer.path().join("does-not-exist");
1465 let err = Vfs::external(missing).expect_err("must reject missing path");
1466 assert!(
1467 err.to_string().contains("VFS root"),
1468 "error should mention VFS root: {err}"
1469 );
1470 }
1471
1472 #[test]
1473 fn real_directory_yields_a_usable_handle() {
1474 let outer = tempfile::tempdir().expect("outer tempdir");
1475 std::fs::write(outer.path().join("a.txt"), b"hi").expect("write");
1476 let vfs = Vfs::external(outer.path().to_path_buf()).expect("external");
1477 let dir = vfs.dir().expect("an external root backs a directory");
1478 assert_eq!(dir.read("a.txt").expect("read through handle"), b"hi");
1479 }
1480
1481 #[test]
1482 fn none_backs_no_handle() {
1483 let vfs = Vfs::none();
1484 assert!(vfs.dir().is_none());
1485 assert_eq!(vfs.mode(), VfsMode::None);
1486 }
1487
1488 #[test]
1489 fn tempdir_modes_carry_a_handle() {
1490 let vfs = Vfs::tempdir().expect("tempdir");
1491 assert!(
1492 vfs.dir().is_some(),
1493 "ephemeral roots resolve through a handle too"
1494 );
1495 }
1496
1497 #[test]
1498 fn handle_survives_a_rename_of_the_root() {
1499 let outer = tempfile::tempdir().expect("outer tempdir");
1500 let from = outer.path().join("before");
1501 let to = outer.path().join("after");
1502 std::fs::create_dir(&from).expect("mkdir");
1503 std::fs::write(from.join("a.txt"), b"hi").expect("write");
1504
1505 let vfs = Vfs::external(from.clone()).expect("external");
1506 std::fs::rename(&from, &to).expect("rename");
1507
1508 let dir = vfs.dir().expect("handle");
1511 assert_eq!(dir.read("a.txt").expect("read after rename"), b"hi");
1512 }
1513
1514 #[test]
1515 fn external_rejects_file() {
1516 let outer = tempfile::tempdir().expect("outer tempdir");
1517 let file = outer.path().join("a.txt");
1518 std::fs::write(&file, b"hi").expect("write");
1519 let err = Vfs::external(file).expect_err("must reject file");
1520 assert!(
1521 err.to_string().contains("not a directory"),
1522 "error should mention not a directory: {err}"
1523 );
1524 }
1525
1526 #[test]
1527 fn size_limit_starts_from_what_the_directory_holds() {
1528 let outer = tempfile::tempdir().expect("outer tempdir");
1529 std::fs::write(outer.path().join("a.txt"), vec![0u8; 300]).expect("write");
1530 std::fs::create_dir(outer.path().join("sub")).expect("mkdir");
1531 std::fs::write(outer.path().join("sub/b.txt"), vec![0u8; 200]).expect("write");
1532 #[cfg(unix)]
1533 std::os::unix::fs::symlink("a.txt", outer.path().join("link")).expect("symlink");
1534
1535 let vfs = Vfs::external_with_mode(outer.path().to_path_buf(), VfsMode::PerSession)
1536 .expect("external")
1537 .with_size_limit(1_000);
1538 let quota = vfs.quota().expect("quota attached");
1539 assert_eq!(quota.used(), 500, "links count as nothing");
1540 assert_eq!(quota.limit(), 1_000);
1541 assert!(
1542 vfs.clone().quota().is_some_and(|q| Arc::ptr_eq(q, quota)),
1543 "clones share it"
1544 );
1545 }
1546
1547 #[test]
1548 fn measuring_many_directories_holds_one_open_per_level() {
1549 let outer = tempfile::tempdir().expect("outer tempdir");
1550 let mut path = outer.path().to_path_buf();
1551 for depth in 0..3_000 {
1552 if depth % 60 == 0 {
1553 path = outer.path().join(format!("branch{depth}"));
1554 } else {
1555 path = path.join("d");
1556 }
1557 std::fs::create_dir_all(&path).expect("mkdir");
1558 }
1559 std::fs::write(path.join("leaf.txt"), b"12345").expect("write");
1560 let root = Vfs::external(outer.path().to_path_buf()).expect("external");
1561 assert_eq!(root.measure_usage().expect("measure"), 5);
1562 }
1563
1564 #[test]
1565 fn listing_files_stops_past_its_entry_cap() {
1566 let outer = tempfile::tempdir().expect("outer tempdir");
1567 std::fs::write(outer.path().join("a"), b"1").expect("write");
1568 std::fs::create_dir(outer.path().join("d")).expect("mkdir");
1569 let root = Vfs::external(outer.path().to_path_buf()).expect("external");
1570 let dir = root.dir().expect("dir");
1571 assert_eq!(regular_files(dir, 2).expect("two entries fit").len(), 1);
1572 std::fs::write(outer.path().join("d/b"), b"2").expect("write");
1573 let error = regular_files(dir, 2).expect_err("a third entry passes the cap");
1574 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
1575 }
1576
1577 #[test]
1578 fn a_tree_nested_past_the_measured_depth_opens_as_full() {
1579 let outer = tempfile::tempdir().expect("outer tempdir");
1580 let mut path = outer.path().to_path_buf();
1581 for _ in 0..MAX_MEASURED_DEPTH {
1582 path = path.join("d");
1583 }
1584 std::fs::create_dir_all(&path).expect("mkdir");
1585 let fits = Vfs::external(outer.path().to_path_buf()).expect("external");
1586 assert_eq!(fits.measure_usage().expect("measure"), 0);
1587
1588 std::fs::create_dir(path.join("d")).expect("mkdir");
1589 let vfs = Vfs::external(outer.path().to_path_buf())
1590 .expect("external")
1591 .with_size_limit(1_000);
1592 assert!(vfs.quota().is_some_and(|quota| quota.is_unmeasured()));
1593 }
1594
1595 #[cfg(unix)]
1596 #[test]
1597 fn a_directory_that_cannot_be_read_opens_as_full() {
1598 use std::os::unix::fs::PermissionsExt;
1599 let outer = tempfile::tempdir().expect("outer tempdir");
1600 let locked = outer.path().join("locked");
1601 std::fs::create_dir(&locked).expect("mkdir");
1602 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod");
1603 let vfs = Vfs::external(outer.path().to_path_buf())
1604 .expect("external")
1605 .with_size_limit(1_000);
1606 let unmeasured = vfs.quota().is_some_and(|quota| quota.is_unmeasured());
1607 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755))
1608 .expect("chmod back");
1609 assert!(unmeasured, "a failed walk leaves the VFS treated as full");
1610 }
1611
1612 #[test]
1613 fn only_a_walk_that_skips_vanished_entries_survives_one() {
1614 let gone = || io::Result::<u8>::Err(io::ErrorKind::NotFound.into());
1615 assert!(vanished(gone(), false).is_err());
1617 assert_eq!(vanished(gone(), true).expect("skipped"), None);
1618 let denied = io::Result::<u8>::Err(io::ErrorKind::PermissionDenied.into());
1619 assert!(vanished(denied, true).is_err());
1620 assert_eq!(vanished(Ok(1u8), false).expect("ok"), Some(1));
1621 let dir = tempfile::tempdir().expect("dir");
1622 std::fs::write(dir.path().join("a"), "abc").expect("write");
1623 assert_eq!(measure_host_dir(dir.path()).expect("strict"), 3);
1624 assert_eq!(
1625 measure_host_dir_skipping_vanished(dir.path()).expect("lenient"),
1626 3
1627 );
1628 }
1629
1630 fn copied(from: &Path, cap: u64) -> (tempfile::TempDir, Result<u64, CopyDirError>) {
1631 let to = tempfile::tempdir().expect("to");
1632 let mut bytes = 0;
1633 let result = copy_host_dir(from, &to.path().join("out"), cap, &mut bytes).map(|()| bytes);
1634 (to, result)
1635 }
1636
1637 #[test]
1638 fn a_copy_has_the_files_and_stops_at_the_cap() {
1639 let from = tempfile::tempdir().expect("from");
1640 std::fs::create_dir(from.path().join("sub")).expect("mkdir");
1641 std::fs::write(from.path().join("sub/a.txt"), "hello").expect("write");
1642 let (to, result) = copied(from.path(), 100);
1643 assert_eq!(result.expect("copied"), 5);
1644 assert_eq!(
1645 std::fs::read_to_string(to.path().join("out/sub/a.txt")).expect("read"),
1646 "hello"
1647 );
1648 let (_to, result) = copied(from.path(), 4);
1649 assert!(matches!(result, Err(CopyDirError::OverCap)));
1650 }
1651
1652 #[cfg(unix)]
1653 #[test]
1654 fn a_copy_recreates_links_and_never_follows_them() {
1655 let outside = tempfile::tempdir().expect("outside");
1656 std::fs::write(outside.path().join("secret"), "host file").expect("write");
1657 std::fs::create_dir(outside.path().join("dir")).expect("mkdir");
1658 std::fs::write(outside.path().join("dir/inner"), "host dir file").expect("write");
1659 let from = tempfile::tempdir().expect("from");
1660 std::os::unix::fs::symlink(outside.path().join("secret"), from.path().join("file-link"))
1661 .expect("symlink");
1662 std::os::unix::fs::symlink(outside.path().join("dir"), from.path().join("dir-link"))
1663 .expect("symlink");
1664 let (to, result) = copied(from.path(), 1_000);
1665 assert_eq!(result.expect("copied"), 0, "no linked content is copied");
1666 for name in ["file-link", "dir-link"] {
1667 let copy = to.path().join("out").join(name);
1668 assert!(std::fs::symlink_metadata(©).expect("kept").is_symlink());
1669 }
1670 assert!(!to.path().join("out/dir-link-copy").exists());
1671 }
1672
1673 #[cfg(unix)]
1674 #[test]
1675 fn an_entry_that_is_a_link_when_opened_is_not_followed() {
1676 let outside = tempfile::tempdir().expect("outside");
1677 std::fs::write(outside.path().join("secret"), "host file").expect("write");
1678 std::fs::create_dir(outside.path().join("dir")).expect("mkdir");
1679 let from = tempfile::tempdir().expect("from");
1680 let to = tempfile::tempdir().expect("to");
1681 std::os::unix::fs::symlink(outside.path().join("secret"), from.path().join("swapped"))
1682 .expect("symlink");
1683 std::os::unix::fs::symlink(outside.path().join("dir"), from.path().join("swapped-dir"))
1684 .expect("symlink");
1685 let source = Dir::open_ambient_dir(from.path(), ambient_authority()).expect("source");
1686 let target = Dir::open_ambient_dir(to.path(), ambient_authority()).expect("target");
1687 let mut bytes = 0;
1688 let mut copy = TreeCopy {
1689 cap: 1_000,
1690 bytes: &mut bytes,
1691 entries: 0,
1692 rel: PathBuf::new(),
1693 };
1694 copy.entry(&source, &target, OsStr::new("swapped"), EntryKind::File, 0)
1696 .expect("file");
1697 copy.entry(
1698 &source,
1699 &target,
1700 OsStr::new("swapped-dir"),
1701 EntryKind::Dir,
1702 0,
1703 )
1704 .expect("dir");
1705 assert_eq!(bytes, 0);
1706 for name in ["swapped", "swapped-dir"] {
1707 let copy = to.path().join(name);
1708 assert!(std::fs::symlink_metadata(©).expect("kept").is_symlink());
1709 }
1710 }
1711
1712 #[test]
1713 fn an_entry_removed_since_it_was_listed_is_skipped() {
1714 let from = tempfile::tempdir().expect("from");
1715 let to = tempfile::tempdir().expect("to");
1716 let source = Dir::open_ambient_dir(from.path(), ambient_authority()).expect("source");
1717 let target = Dir::open_ambient_dir(to.path(), ambient_authority()).expect("target");
1718 let mut bytes = 0;
1719 let mut copy = TreeCopy {
1720 cap: 1_000,
1721 bytes: &mut bytes,
1722 entries: 0,
1723 rel: PathBuf::new(),
1724 };
1725 for kind in [EntryKind::Dir, EntryKind::File, EntryKind::Link] {
1726 copy.entry(&source, &target, OsStr::new("gone"), kind, 0)
1727 .expect("skipped");
1728 }
1729 assert_eq!(std::fs::read_dir(to.path()).expect("list").count(), 0);
1730 }
1731
1732 #[test]
1733 fn an_entry_that_changed_kind_since_it_was_listed_is_copied_as_what_it_is() {
1734 let from = tempfile::tempdir().expect("from");
1735 let to = tempfile::tempdir().expect("to");
1736 std::fs::write(from.path().join("was-dir"), "abc").expect("write");
1737 std::fs::write(from.path().join("was-link"), "de").expect("write");
1738 let source = Dir::open_ambient_dir(from.path(), ambient_authority()).expect("source");
1739 let target = Dir::open_ambient_dir(to.path(), ambient_authority()).expect("target");
1740 let mut bytes = 0;
1741 let mut copy = TreeCopy {
1742 cap: 1_000,
1743 bytes: &mut bytes,
1744 entries: 0,
1745 rel: PathBuf::new(),
1746 };
1747 copy.entry(&source, &target, OsStr::new("was-dir"), EntryKind::Dir, 0)
1748 .expect("dir now a file");
1749 copy.entry(&source, &target, OsStr::new("was-link"), EntryKind::Link, 0)
1750 .expect("link now a file");
1751 copy.entry(&source, &target, OsStr::new("was-dir"), EntryKind::File, 0)
1753 .expect("duplicate");
1754 assert_eq!(bytes, 5);
1755 assert_eq!(
1756 std::fs::read_to_string(to.path().join("was-link")).expect("read"),
1757 "de"
1758 );
1759 }
1760
1761 #[cfg(unix)]
1762 #[test]
1763 fn an_entry_that_changes_kind_a_second_time_is_skipped_not_chased() {
1764 let outside = tempfile::tempdir().expect("outside");
1765 let from = tempfile::tempdir().expect("from");
1766 let to = tempfile::tempdir().expect("to");
1767 std::os::unix::fs::symlink(outside.path(), from.path().join("now-link")).expect("symlink");
1768 std::fs::write(from.path().join("now-file"), "abc").expect("write");
1769 let source = Dir::open_ambient_dir(from.path(), ambient_authority()).expect("source");
1770 let target = Dir::open_ambient_dir(to.path(), ambient_authority()).expect("target");
1771 let mut bytes = 0;
1772 let mut copy = TreeCopy {
1773 cap: 1_000,
1774 bytes: &mut bytes,
1775 entries: 0,
1776 rel: PathBuf::new(),
1777 };
1778 copy.file(&source, &target, OsStr::new("now-link"), false)
1780 .expect("re-classified once");
1781 assert!(
1782 std::fs::symlink_metadata(to.path().join("now-link"))
1783 .expect("kept")
1784 .is_symlink()
1785 );
1786 copy.link(&source, &target, OsStr::new("now-file"), true)
1789 .expect("skipped");
1790 copy.file(&source, &target, OsStr::new("now-link"), true)
1791 .expect("skipped");
1792 assert!(!to.path().join("now-file").exists());
1793 assert_eq!(bytes, 0);
1794 }
1795
1796 #[cfg(unix)]
1797 #[test]
1798 fn an_entry_that_flipped_back_is_skipped_but_an_unreadable_one_still_fails() {
1799 use std::os::unix::fs::PermissionsExt;
1800 let from = tempfile::tempdir().expect("from");
1801 std::fs::write(from.path().join("plain"), "abc").expect("write");
1802 std::fs::create_dir(from.path().join("dir")).expect("mkdir");
1803 let locked = from.path().join("locked");
1804 std::fs::write(&locked, "x").expect("write");
1805 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod");
1806 let source = Dir::open_ambient_dir(from.path(), ambient_authority()).expect("source");
1807 let to = tempfile::tempdir().expect("to");
1808 let target = Dir::open_ambient_dir(to.path(), ambient_authority()).expect("target");
1809 let mut bytes = 0;
1810 let mut copy = TreeCopy {
1811 cap: 1_000,
1812 bytes: &mut bytes,
1813 entries: 0,
1814 rel: PathBuf::new(),
1815 };
1816 copy.entry(&source, &target, OsStr::new("plain"), EntryKind::Dir, 0)
1819 .expect("file listed as dir, re-classified");
1820 let not_a_dir = io::Error::from_raw_os_error(libc::ENOTDIR);
1821 copy.changed(
1822 &source,
1823 &target,
1824 OsStr::new("dir"),
1825 EntryKind::Dir,
1826 not_a_dir,
1827 false,
1828 )
1829 .expect("flipped back to a directory");
1830 let not_a_link = io::Error::from_raw_os_error(libc::EINVAL);
1831 copy.changed(
1832 &source,
1833 &target,
1834 OsStr::new("plain"),
1835 EntryKind::File,
1836 not_a_link,
1837 false,
1838 )
1839 .expect("flipped back to a file");
1840 let loop_error = io::Error::from_raw_os_error(libc::ELOOP);
1841 copy.changed(
1842 &source,
1843 &target,
1844 OsStr::new("plain"),
1845 EntryKind::Link,
1846 loop_error,
1847 true,
1848 )
1849 .expect("reclassified, wrong kind");
1850 if unsafe { libc::geteuid() } != 0 {
1852 let result = copy.file(&source, &target, OsStr::new("locked"), true);
1854 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o600))
1855 .expect("chmod");
1856 let Err(CopyDirError::Io(error)) = result else {
1857 panic!("expected an I/O error, got {result:?}");
1858 };
1859 assert!(error.to_string().starts_with("locked: "), "{error}");
1860 }
1861 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o600)).expect("chmod");
1862 let denied = io::Error::from_raw_os_error(libc::EIO);
1864 let result = copy.changed(
1865 &source,
1866 &target,
1867 OsStr::new("dir"),
1868 EntryKind::Dir,
1869 denied,
1870 false,
1871 );
1872 assert!(matches!(result, Err(CopyDirError::Io(_))));
1873 }
1874
1875 #[cfg(unix)]
1876 #[test]
1877 fn an_unreadable_entry_fails_the_copy_naming_its_path_in_the_tree() {
1878 use std::os::unix::fs::PermissionsExt;
1879 let from = tempfile::tempdir().expect("from");
1880 std::fs::create_dir(from.path().join("sub")).expect("mkdir");
1881 let locked = from.path().join("sub/locked");
1882 std::fs::write(&locked, "x").expect("write");
1883 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod");
1884 let (_to, result) = copied(from.path(), 1_000);
1885 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o600)).expect("chmod");
1886 if unsafe { libc::geteuid() } == 0 {
1888 return;
1889 }
1890 let Err(CopyDirError::Io(error)) = result else {
1891 panic!("expected an I/O error, got {result:?}");
1892 };
1893 let message = error.to_string();
1894 assert!(message.starts_with("sub/locked: "), "{message}");
1895 assert!(!message.contains(from.path().to_str().expect("utf8")));
1896 }
1897
1898 #[test]
1899 fn a_missing_component_opens_as_none() {
1900 let volume = tempfile::tempdir().expect("volume");
1901 std::fs::create_dir_all(volume.path().join("a")).expect("mkdir");
1902 assert!(
1903 open_host_subdir(volume.path(), Path::new("a"))
1904 .unwrap()
1905 .is_some()
1906 );
1907 assert!(
1908 open_host_subdir(volume.path(), Path::new("a/b/c"))
1909 .unwrap()
1910 .is_none()
1911 );
1912 assert!(
1913 open_host_subdir(volume.path(), Path::new("x"))
1914 .unwrap()
1915 .is_none()
1916 );
1917 assert_eq!(
1918 measure_host_subdir_skipping_vanished(volume.path(), Path::new("a/b")).unwrap(),
1919 0
1920 );
1921 }
1922
1923 #[cfg(unix)]
1924 #[test]
1925 fn a_link_component_is_an_error() {
1926 let volume = tempfile::tempdir().expect("volume");
1927 let outside = tempfile::tempdir().expect("outside");
1928 std::fs::create_dir_all(outside.path().join("deep")).expect("mkdir");
1929 std::fs::write(outside.path().join("deep/f"), "x").expect("write");
1930 std::fs::create_dir_all(volume.path().join("a")).expect("mkdir");
1931 std::os::unix::fs::symlink(outside.path(), volume.path().join("a/link")).expect("link");
1932 for sub in ["a/link", "a/link/deep"] {
1933 let error = open_host_subdir(volume.path(), Path::new(sub)).expect_err("refused");
1934 assert!(error.to_string().starts_with(sub), "{error}");
1935 assert!(measure_host_subdir_skipping_vanished(volume.path(), Path::new(sub)).is_err());
1936 let to = tempfile::tempdir().expect("to");
1937 let result = copy_host_subdir(volume.path(), Path::new(sub), to.path(), 1_000, &mut 0);
1938 assert!(matches!(result, Err(CopyDirError::Io(_))));
1939 }
1940 }
1941
1942 #[test]
1943 fn a_sub_path_is_copied_to_the_same_place_under_the_target() {
1944 let from = tempfile::tempdir().expect("from");
1945 std::fs::create_dir_all(from.path().join("users/ada/notes")).expect("mkdir");
1946 std::fs::write(from.path().join("users/ada/notes/n.txt"), "note").expect("write");
1947 std::fs::write(from.path().join("users/ada/a.txt"), "ada").expect("write");
1948 std::fs::write(from.path().join("users/b.txt"), "bob").expect("write");
1949 std::fs::write(from.path().join("top.txt"), "top").expect("write");
1950 let to = tempfile::tempdir().expect("to");
1951 let mut bytes = 0;
1952 copy_host_subdir(
1953 from.path(),
1954 Path::new("users/ada"),
1955 to.path(),
1956 1_000,
1957 &mut bytes,
1958 )
1959 .expect("copied");
1960 assert_eq!(bytes, 7);
1961 let read = |rel: &str| std::fs::read_to_string(to.path().join(rel)).expect(rel);
1962 assert_eq!(read("users/ada/notes/n.txt"), "note");
1963 assert_eq!(read("users/ada/a.txt"), "ada");
1964 assert!(!to.path().join("users/b.txt").exists());
1965 assert!(!to.path().join("top.txt").exists());
1966 assert_eq!(
1967 measure_host_subdir_skipping_vanished(from.path(), Path::new("users/ada")).unwrap(),
1968 7
1969 );
1970 }
1971
1972 #[test]
1973 fn a_copy_stops_in_a_tree_nested_too_deep() {
1974 let from = tempfile::tempdir().expect("from");
1975 let mut path = from.path().to_path_buf();
1976 for _ in 0..=MAX_MEASURED_DEPTH + 1 {
1977 path = path.join("d");
1978 }
1979 std::fs::create_dir_all(&path).expect("mkdir");
1980 let (_to, result) = copied(from.path(), 1_000);
1981 assert!(matches!(result, Err(CopyDirError::TooMany)));
1982 }
1983}