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 self,
477 spec: MountSpec,
478 sub_path: Option<&str>,
479 ) -> Result<Self, MountError> {
480 let access = spec.access;
481 let root_access = self.access;
482 self.open_mount(spec, sub_path, root_access, access)
483 }
484
485 pub fn reopen_mount_subpath(
487 self,
488 spec: MountSpec,
489 sub_path: Option<&str>,
490 ) -> Result<Self, MountError> {
491 self.open_mount(spec, sub_path, Access::ReadOnly, Access::ReadOnly)
492 }
493
494 fn open_mount(
495 mut self,
496 spec: MountSpec,
497 sub_path: Option<&str>,
498 root_preparation: Access,
499 volume_preparation: Access,
500 ) -> Result<Self, MountError> {
501 let root = self.dir.as_ref().ok_or(MountError::RootDisabled)?;
502 if self.mounts.len() >= MAX_MOUNTS {
503 return Err(MountError::TooMany);
504 }
505 let rel = mount_rel(&spec.guest_path)?;
506 if crate::runtime::fs::protected_metadata(&rel) {
507 return Err(MountError::ProtectedPath(spec.guest_path));
508 }
509 for mount in self.mounts.iter() {
512 let (outer, inner) = if starts_with_folded(&rel, &mount.rel) {
513 (mount.guest.to_string(), spec.guest_path.clone())
514 } else if starts_with_folded(&mount.rel, &rel) {
515 (spec.guest_path.clone(), mount.guest.to_string())
516 } else {
517 continue;
518 };
519 return Err(MountError::Nested { outer, inner });
520 }
521 let placeholders = prepare_mount_point(root, &rel, root_preparation, &spec.guest_path)?;
522 let (dir, ancestors) = select_directory(
523 open_volume(&spec.host).map_err(MountError::Io)?,
524 sub_path,
525 volume_preparation,
526 )
527 .map_err(MountError::Io)?;
528 let mount = Mount {
529 guest: Arc::from(spec.guest_path.as_str()),
530 rel,
531 volume: Arc::from(spec.volume.as_str()),
532 dir,
533 host: Arc::from(
534 sub_path.map_or_else(|| spec.host.clone(), |path| spec.host.join(path)),
535 ),
536 access: spec.access,
537 quota: spec.quota,
538 placeholders: Arc::from(placeholders),
539 ancestors: Arc::from(ancestors),
540 };
541 let mut mounts = self.mounts.to_vec();
542 mounts.push(mount);
543 self.mounts = Arc::from(mounts);
544 Ok(self)
545 }
546
547 pub fn with_mount_quota(mut self, guest_path: &str, quota: Arc<DiskQuota>) -> Self {
551 let mounts: Vec<Mount> = self
552 .mounts
553 .iter()
554 .cloned()
555 .map(|mut mount| {
556 if &*mount.guest == guest_path {
557 mount.quota = Some(Arc::clone("a));
558 }
559 mount
560 })
561 .collect();
562 self.mounts = Arc::from(mounts);
563 self
564 }
565
566 pub fn locate(&self, rel: &Path) -> Option<(Arc<Dir>, PathBuf, Placement)> {
574 let root = self.dir.as_ref()?;
575 let (protected_roots, protected_ancestors) = self.protected_directories();
576 for mount in self.mounts.iter() {
577 if let Ok(rest) = rel.strip_prefix(&mount.rel) {
578 let rest = if rest.as_os_str().is_empty() {
579 PathBuf::from(".")
580 } else {
581 rest.to_path_buf()
582 };
583 let placement = Placement {
584 access: mount.access,
585 quota: mount.quota.clone(),
586 mount: Some(Arc::clone(&mount.guest)),
587 nested: Arc::from([]),
588 host: Arc::clone(&mount.host),
589 volume: Some(Arc::clone(&mount.volume)),
590 ancestors: Arc::clone(&mount.ancestors),
591 protected_roots,
592 protected_ancestors,
593 };
594 return Some((Arc::clone(&mount.dir), rest, placement));
595 }
596 }
597 let placement = Placement {
598 access: self.access,
599 quota: self.quota.clone(),
600 mount: None,
601 nested: Arc::clone(&self.mounts),
602 host: Arc::from(self.root.as_path()),
603 volume: self.volume.clone(),
604 ancestors: Arc::clone(&self.ancestors),
605 protected_roots,
606 protected_ancestors,
607 };
608 Some((Arc::clone(root), rel.to_path_buf(), placement))
609 }
610
611 fn protected_directories(&self) -> (Arc<[FileIdentity]>, Arc<[FileIdentity]>) {
612 let mut roots = Vec::new();
613 let mut ancestors = self
614 .ancestors
615 .split_last()
616 .map_or(&[][..], |(_, parents)| parents)
617 .to_vec();
618 if let Some(identity) = self.ancestors.last() {
619 roots.push(*identity);
620 if !self.mounts.is_empty() {
621 ancestors.push(*identity);
622 }
623 }
624 for mount in self.mounts.iter() {
625 if let Some(identity) = mount.ancestors.last() {
626 roots.push(*identity);
627 }
628 if let Some((_, parents)) = mount.ancestors.split_last() {
629 ancestors.extend_from_slice(parents);
630 }
631 for placeholder in mount.placeholders.iter() {
633 ancestors.push(placeholder.identity);
634 }
635 if let Some(placeholder) = mount.placeholders.last() {
636 roots.push(placeholder.identity);
637 }
638 }
639 (Arc::from(roots), Arc::from(ancestors))
640 }
641
642 pub fn cwd(&self) -> &str {
644 &self.cwd
645 }
646
647 pub fn with_cwd(self, cwd: &str) -> Result<Self, crate::runtime::fs::ContainError> {
648 use crate::runtime::fs::{ContainError, resolve_content};
649 let vfs = self.with_cwd_path(cwd)?;
650 let target = resolve_content(&vfs, "/", cwd)?;
651 match target.open_dir() {
652 Ok(_) => {}
653 Err(ContainError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
654 target.create_dir_all()?;
655 target.open_dir()?;
656 }
657 Err(error) => return Err(error),
658 }
659 Ok(vfs)
660 }
661
662 pub fn with_cwd_path(mut self, cwd: &str) -> Result<Self, crate::runtime::fs::ContainError> {
664 use crate::runtime::fs::{ContainError, guest_normalize};
665 if !cwd.starts_with('/') || cwd.len() > 4096 || guest_normalize("/", cwd)? != cwd {
666 return Err(ContainError::Io(io::Error::new(
667 io::ErrorKind::InvalidInput,
668 "cwd must be an absolute normalized guest path",
669 )));
670 }
671 self.cwd = Arc::from(cwd);
672 Ok(self)
673 }
674
675 pub fn with_subpath(self, sub_path: Option<&str>) -> io::Result<Self> {
677 let access = self.access;
678 self.select_subpath(sub_path, access)
679 }
680
681 pub fn reopen_subpath(self, sub_path: Option<&str>) -> io::Result<Self> {
683 self.select_subpath(sub_path, Access::ReadOnly)
684 }
685
686 fn select_subpath(mut self, sub_path: Option<&str>, preparation: Access) -> io::Result<Self> {
687 let root = self
688 .dir
689 .clone()
690 .ok_or_else(|| io::Error::other("filesystem disabled"))?;
691 let (dir, ancestors) = select_directory(root, sub_path, preparation)?;
692 self.dir = Some(dir);
693 if let Some(path) = sub_path {
694 self.root.push(path);
695 }
696 self.ancestors = Arc::from(ancestors);
697 Ok(self)
698 }
699
700 pub fn measure_usage(&self) -> io::Result<u64> {
702 match &self.dir {
703 Some(root) => measure_dir(root),
704 None => Ok(0),
705 }
706 }
707}
708
709pub fn measure_host_dir(root: &Path) -> io::Result<u64> {
713 measure_dir(&*open_volume(root)?)
714}
715
716pub fn measure_host_dir_skipping_vanished(root: &Path) -> io::Result<u64> {
719 let mut total: u64 = 0;
720 let root = open_volume(root)?;
721 for_each_regular_file(&root, MAX_MEASURED_ENTRIES, true, |_, bytes| {
722 total = total.saturating_add(bytes);
723 })?;
724 Ok(total)
725}
726
727pub fn measure_host_subdir_skipping_vanished(volume: &Path, sub: &Path) -> io::Result<u64> {
730 let Some(root) = open_host_subdir(volume, sub)? else {
731 return Ok(0);
732 };
733 let mut total: u64 = 0;
734 for_each_regular_file(&root, MAX_MEASURED_ENTRIES, true, |_, bytes| {
735 total = total.saturating_add(bytes);
736 })?;
737 Ok(total)
738}
739
740pub fn open_host_subdir(volume: &Path, sub: &Path) -> io::Result<Option<Arc<Dir>>> {
744 let mut dir = open_volume(volume)?;
745 for component in sub.components() {
746 let Component::Normal(name) = component else {
747 return Err(io::Error::new(
748 io::ErrorKind::InvalidInput,
749 "a sub-path must be a normalized relative path",
750 ));
751 };
752 dir = match dir.open_dir_nofollow(name) {
753 Ok(child) => Arc::new(child),
754 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
755 Err(error) => {
756 return Err(io::Error::new(
757 error.kind(),
758 format!("{}: {error}", sub.display()),
759 ));
760 }
761 };
762 }
763 Ok(Some(dir))
764}
765
766pub fn measure_dir(root: &Dir) -> io::Result<u64> {
768 let mut total: u64 = 0;
769 for_each_regular_file(root, MAX_MEASURED_ENTRIES, false, |_, bytes| {
770 total = total.saturating_add(bytes);
771 })?;
772 Ok(total)
773}
774
775pub fn regular_files(root: &Dir, max_entries: usize) -> io::Result<Vec<(FileIdentity, u64)>> {
778 let mut files = Vec::new();
779 for_each_regular_file(root, max_entries, false, |file, bytes| {
780 files.push((file, bytes));
781 })?;
782 Ok(files)
783}
784
785fn for_each_regular_file(
792 root: &Dir,
793 max_entries: usize,
794 skip_vanished: bool,
795 mut visit: impl FnMut(FileIdentity, u64),
796) -> io::Result<()> {
797 let mut entries: usize = 0;
798 let mut open: Vec<cap_std::fs::ReadDir> = vec![root.entries()?];
799 while let Some(dir) = open.last_mut() {
800 let Some(entry) = dir.next() else {
801 open.pop();
802 continue;
803 };
804 let entry = entry?;
805 entries += 1;
806 if entries > max_entries {
807 return Err(io::Error::new(
808 io::ErrorKind::InvalidData,
809 format!("more than {max_entries} entries"),
810 ));
811 }
812 let Some(kind) = vanished(entry.file_type(), skip_vanished)? else {
813 continue;
814 };
815 if kind.is_dir() {
816 if open.len() > MAX_MEASURED_DEPTH {
817 return Err(io::Error::new(
818 io::ErrorKind::InvalidData,
819 format!("directories nested more than {MAX_MEASURED_DEPTH} deep"),
820 ));
821 }
822 let Some(child) = vanished(
823 entry.open_dir().and_then(|dir| dir.entries()),
824 skip_vanished,
825 )?
826 else {
827 continue;
828 };
829 open.push(child);
830 } else if kind.is_file() {
831 let Some(metadata) = vanished(
834 cap_fs_ext::DirEntryExt::full_metadata(&entry),
835 skip_vanished,
836 )?
837 else {
838 continue;
839 };
840 visit(FileIdentity::of(&metadata)?, metadata.len());
841 }
842 }
843 Ok(())
844}
845
846fn vanished<T>(result: io::Result<T>, skip: bool) -> io::Result<Option<T>> {
848 match result {
849 Ok(value) => Ok(Some(value)),
850 Err(error) if skip && error.kind() == io::ErrorKind::NotFound => Ok(None),
851 Err(error) => Err(error),
852 }
853}
854
855#[derive(Debug)]
857pub enum CopyDirError {
858 Io(io::Error),
859 OverCap,
861 TooMany,
863}
864
865impl From<io::Error> for CopyDirError {
866 fn from(error: io::Error) -> Self {
867 Self::Io(error)
868 }
869}
870
871pub fn copy_host_dir(
880 from: &Path,
881 to: &Path,
882 cap: u64,
883 bytes: &mut u64,
884) -> Result<(), CopyDirError> {
885 let source = open_volume(from)?;
886 std::fs::create_dir_all(to)?;
887 let target = Dir::open_ambient_dir(to, ambient_authority())?;
888 let mut copy = TreeCopy {
889 cap,
890 bytes,
891 entries: 0,
892 rel: PathBuf::new(),
893 };
894 copy.dir(&source, &target, 0)
895}
896
897pub fn copy_host_subdir(
902 volume: &Path,
903 sub: &Path,
904 to_root: &Path,
905 cap: u64,
906 bytes: &mut u64,
907) -> Result<(), CopyDirError> {
908 std::fs::create_dir_all(to_root)?;
909 let Some(source) = open_host_subdir(volume, sub)? else {
910 return Ok(());
911 };
912 let mut target = Dir::open_ambient_dir(to_root, ambient_authority())?;
913 for component in sub.components() {
914 target = match target.create_dir(component) {
915 Ok(()) => target.open_dir_nofollow(component)?,
916 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
917 target.open_dir_nofollow(component)?
918 }
919 Err(error) => return Err(error.into()),
920 };
921 }
922 let mut copy = TreeCopy {
923 cap,
924 bytes,
925 entries: 0,
926 rel: sub.to_path_buf(),
927 };
928 copy.dir(&source, &target, 0)
929}
930
931#[derive(Clone, Copy)]
932enum EntryKind {
933 Dir,
934 File,
935 Link,
936 Other,
937}
938
939impl EntryKind {
940 fn of(file_type: cap_std::fs::FileType) -> Self {
941 if file_type.is_dir() {
942 Self::Dir
943 } else if file_type.is_file() {
944 Self::File
945 } else if file_type.is_symlink() {
946 Self::Link
947 } else {
948 Self::Other
949 }
950 }
951}
952
953struct TreeCopy<'a> {
954 cap: u64,
955 bytes: &'a mut u64,
956 entries: usize,
957 rel: PathBuf,
959}
960
961impl TreeCopy<'_> {
962 fn at(&self, name: &OsStr, error: io::Error) -> CopyDirError {
964 let path = self.rel.join(name);
965 let shown = if path.as_os_str().is_empty() {
966 Path::new(".")
967 } else {
968 &path
969 };
970 io::Error::new(error.kind(), format!("{}: {error}", shown.display())).into()
971 }
972
973 fn dir(&mut self, source: &Dir, target: &Dir, depth: usize) -> Result<(), CopyDirError> {
974 let entries = source
975 .entries()
976 .map_err(|error| self.at(OsStr::new(""), error))?;
977 for entry in entries {
978 let entry = entry.map_err(|error| self.at(OsStr::new(""), error))?;
979 self.entries += 1;
980 if self.entries > MAX_MEASURED_ENTRIES {
981 return Err(CopyDirError::TooMany);
982 }
983 let Some(file_type) = vanished(entry.file_type(), true)? else {
984 continue;
985 };
986 let kind = EntryKind::of(file_type);
987 self.entry(source, target, &entry.file_name(), kind, depth)?;
988 }
989 Ok(())
990 }
991
992 fn entry(
995 &mut self,
996 source: &Dir,
997 target: &Dir,
998 name: &OsStr,
999 kind: EntryKind,
1000 depth: usize,
1001 ) -> Result<(), CopyDirError> {
1002 match kind {
1003 EntryKind::Dir => self.subdir(source, target, name, depth),
1004 EntryKind::File => self.file(source, target, name, false),
1005 EntryKind::Link => self.link(source, target, name, false),
1006 EntryKind::Other => Ok(()),
1007 }
1008 }
1009
1010 fn subdir(
1011 &mut self,
1012 source: &Dir,
1013 target: &Dir,
1014 name: &OsStr,
1015 depth: usize,
1016 ) -> Result<(), CopyDirError> {
1017 if depth >= MAX_MEASURED_DEPTH {
1018 return Err(CopyDirError::TooMany);
1019 }
1020 let child = match source.open_dir_nofollow(name) {
1021 Ok(child) => child,
1022 Err(error) => return self.changed(source, target, name, EntryKind::Dir, error, false),
1023 };
1024 match target.create_dir(name) {
1025 Ok(()) => {}
1026 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => return Ok(()),
1028 Err(error) => return Err(self.at(name, error)),
1029 }
1030 let copied = target
1031 .open_dir(name)
1032 .map_err(|error| self.at(name, error))?;
1033 self.rel.push(name);
1034 let result = self.dir(&child, &copied, depth + 1);
1035 self.rel.pop();
1036 result
1037 }
1038
1039 fn file(
1041 &mut self,
1042 source: &Dir,
1043 target: &Dir,
1044 name: &OsStr,
1045 reclassified: bool,
1046 ) -> Result<(), CopyDirError> {
1047 let mut options = cap_std::fs::OpenOptions::new();
1048 options.read(true).follow(FollowSymlinks::No);
1049 #[cfg(unix)]
1051 cap_std::fs::OpenOptionsExt::custom_flags(&mut options, libc::O_NONBLOCK | libc::O_NOCTTY);
1052 let mut input = match source.open_with(name, &options) {
1053 Ok(file) => file,
1054 Err(error) => {
1055 return self.changed(source, target, name, EntryKind::File, error, reclassified);
1056 }
1057 };
1058 let metadata = input.metadata().map_err(|error| self.at(name, error))?;
1059 if !metadata.is_file() {
1060 return Ok(());
1061 }
1062 let mut output = match target.open_with(
1063 name,
1064 cap_std::fs::OpenOptions::new().write(true).create_new(true),
1065 ) {
1066 Ok(file) => file,
1067 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => return Ok(()),
1069 Err(error) => return Err(self.at(name, error)),
1070 };
1071 let room = self.cap.saturating_sub(*self.bytes).saturating_add(1);
1073 let copied = io::copy(&mut input.by_ref().take(room), &mut output)
1074 .map_err(|error| self.at(name, error))?;
1075 *self.bytes = self.bytes.saturating_add(copied);
1076 if *self.bytes > self.cap {
1077 return Err(CopyDirError::OverCap);
1078 }
1079 output
1080 .set_permissions(metadata.permissions())
1081 .map_err(|error| self.at(name, error))?;
1082 Ok(())
1083 }
1084
1085 fn link(
1086 &mut self,
1087 source: &Dir,
1088 target: &Dir,
1089 name: &OsStr,
1090 reclassified: bool,
1091 ) -> Result<(), CopyDirError> {
1092 let to = match source.read_link_contents(name) {
1095 Ok(to) => to,
1096 Err(error) => {
1097 return self.changed(source, target, name, EntryKind::Link, error, reclassified);
1098 }
1099 };
1100 #[cfg(not(windows))]
1101 match target.symlink_contents(to, name) {
1102 Ok(()) => {}
1103 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1105 Err(error) => return Err(self.at(name, error)),
1106 }
1107 #[cfg(windows)]
1108 let _ = to;
1109 Ok(())
1110 }
1111
1112 fn changed(
1119 &mut self,
1120 source: &Dir,
1121 target: &Dir,
1122 name: &OsStr,
1123 listed: EntryKind,
1124 error: io::Error,
1125 reclassified: bool,
1126 ) -> Result<(), CopyDirError> {
1127 if error.kind() == io::ErrorKind::NotFound {
1128 return Ok(());
1129 }
1130 if reclassified {
1131 return if wrong_kind(&error) {
1132 Ok(())
1133 } else {
1134 Err(self.at(name, error))
1135 };
1136 }
1137 let now = match source.symlink_metadata(name) {
1138 Ok(meta) => EntryKind::of(meta.file_type()),
1139 Err(gone) if gone.kind() == io::ErrorKind::NotFound => return Ok(()),
1140 Err(_) => return Err(self.at(name, error)),
1141 };
1142 match (listed, now) {
1143 (EntryKind::Dir | EntryKind::Link, EntryKind::File) => {
1144 self.file(source, target, name, true)
1145 }
1146 (EntryKind::Dir | EntryKind::File, EntryKind::Link) => {
1147 self.link(source, target, name, true)
1148 }
1149 (EntryKind::File | EntryKind::Link, EntryKind::Dir) | (_, EntryKind::Other) => Ok(()),
1151 _ if wrong_kind(&error) => Ok(()),
1154 _ => Err(self.at(name, error)),
1155 }
1156 }
1157}
1158
1159fn wrong_kind(error: &io::Error) -> bool {
1162 #[cfg(unix)]
1163 {
1164 matches!(
1165 error.raw_os_error(),
1166 Some(libc::ENOTDIR | libc::ELOOP | libc::EINVAL)
1167 )
1168 }
1169 #[cfg(not(unix))]
1170 {
1171 let _ = error;
1172 false
1173 }
1174}
1175
1176const MAX_MEASURED_ENTRIES: usize = 1_000_000;
1179
1180pub const MAX_MEASURED_DEPTH: usize = 64;
1183
1184fn mount_rel(guest: &str) -> Result<PathBuf, MountError> {
1190 let bad = || MountError::BadPath(guest.to_string());
1191 let Some(rest) = guest.strip_prefix('/') else {
1192 return Err(bad());
1193 };
1194 if rest.is_empty() {
1195 return Err(MountError::AtRoot);
1196 }
1197 let mut rel = PathBuf::new();
1198 for part in rest.split('/') {
1199 let plain = part
1200 .chars()
1201 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'));
1202 if part.is_empty() || part.ends_with('.') || !plain {
1205 return Err(bad());
1206 }
1207 rel.push(part);
1208 }
1209 Ok(rel)
1210}
1211
1212fn starts_with_folded(path: &Path, prefix: &Path) -> bool {
1215 let mut parts = path.components();
1216 prefix.components().all(|want| {
1217 parts.next().is_some_and(|have| {
1218 have.as_os_str()
1219 .as_encoded_bytes()
1220 .eq_ignore_ascii_case(want.as_os_str().as_encoded_bytes())
1221 })
1222 })
1223}
1224
1225fn prepare_mount_point(
1231 root: &Dir,
1232 rel: &Path,
1233 access: Access,
1234 guest: &str,
1235) -> Result<Vec<Placeholder>, MountError> {
1236 let unavailable = |reason| MountError::MountPointUnavailable {
1237 path: guest.to_string(),
1238 reason,
1239 };
1240 let mut prefix = PathBuf::new();
1241 for part in rel.components() {
1242 prefix.push(part);
1243 match root.symlink_metadata(&prefix) {
1244 Ok(meta) if meta.is_dir() => {
1245 match spelling_of(root, &prefix).map_err(MountError::RootIo)? {
1246 Spelling::Exact => {}
1247 Spelling::Variant => {
1248 return Err(unavailable(
1249 "exists in the root under a different letter case; rename it to \
1250 match",
1251 ));
1252 }
1253 Spelling::Unknown => {
1254 return Err(unavailable(
1255 "sits beside too many entries to check its spelling; move some \
1256 of them into a subdirectory",
1257 ));
1258 }
1259 }
1260 }
1261 Ok(_) => return Err(unavailable("exists in the root and is not a directory")),
1262 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1263 if access == Access::ReadOnly {
1264 return Err(unavailable(
1265 "does not exist, and a read-only root cannot create it",
1266 ));
1267 }
1268 match root.create_dir(&prefix) {
1269 Ok(()) => {}
1270 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
1272 Err(err) => return Err(MountError::RootIo(err)),
1273 }
1274 }
1275 Err(err) => return Err(MountError::RootIo(err)),
1276 }
1277 }
1278 match root.symlink_metadata(rel) {
1280 Ok(meta) if meta.is_dir() => {}
1281 Ok(_) => return Err(unavailable("exists in the root and is not a directory")),
1282 Err(err) => return Err(MountError::RootIo(err)),
1283 }
1284 placeholders_on(root, rel).map_err(MountError::RootIo)
1285}
1286
1287fn placeholders_on(root: &Dir, rel: &Path) -> io::Result<Vec<Placeholder>> {
1292 let mut placeholders = Vec::new();
1293 let mut prefix = PathBuf::new();
1294 for part in rel.components() {
1295 prefix.push(part);
1296 let meta = root.symlink_metadata(&prefix)?;
1297 placeholders.push(Placeholder {
1298 identity: dir_identity(root, &prefix, &meta)?,
1299 parent: prefix.parent().map(Path::to_path_buf).unwrap_or_default(),
1300 name: part.as_os_str().to_os_string(),
1301 });
1302 }
1303 Ok(placeholders)
1304}
1305
1306enum Spelling {
1308 Exact,
1309 Variant,
1311 Unknown,
1313}
1314
1315fn spelling_of(root: &Dir, path: &Path) -> io::Result<Spelling> {
1326 let Some(name) = path.file_name() else {
1327 return Ok(Spelling::Exact);
1328 };
1329 let flipped: String = name
1330 .to_string_lossy()
1331 .chars()
1332 .map(|c| {
1333 if c.is_ascii_lowercase() {
1334 c.to_ascii_uppercase()
1335 } else {
1336 c.to_ascii_lowercase()
1337 }
1338 })
1339 .collect();
1340 if flipped.as_str() == name {
1341 return Ok(Spelling::Exact);
1342 }
1343 let parent = path.parent().unwrap_or(Path::new(""));
1344 let other = match root.symlink_metadata(parent.join(&flipped)) {
1345 Ok(meta) => meta,
1346 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Spelling::Exact),
1347 Err(err) => return Err(err),
1348 };
1349 let this = root.symlink_metadata(path)?;
1350 if let (Ok(this), Ok(other)) = (FileIdentity::of(&this), FileIdentity::of(&other))
1351 && this != other
1352 {
1353 return Ok(Spelling::Exact);
1354 }
1355 let entries = if parent.as_os_str().is_empty() {
1356 root.entries()?
1357 } else {
1358 root.read_dir(parent)?
1359 };
1360 for (index, entry) in entries.enumerate() {
1361 if index >= MAX_SPELLING_SCAN {
1362 return Ok(Spelling::Unknown);
1363 }
1364 if entry?.file_name() == name {
1365 return Ok(Spelling::Exact);
1366 }
1367 }
1368 Ok(Spelling::Variant)
1369}
1370
1371const MAX_SPELLING_SCAN: usize = 100_000;
1374
1375fn open_volume(host: &Path) -> io::Result<Arc<Dir>> {
1376 let meta = std::fs::metadata(host)?;
1377 if !meta.is_dir() {
1378 return Err(io::Error::new(
1379 io::ErrorKind::NotADirectory,
1380 "volume is not a directory",
1381 ));
1382 }
1383 Dir::open_ambient_dir(host, ambient_authority()).map(Arc::new)
1384}
1385
1386fn select_directory(
1387 mut dir: Arc<Dir>,
1388 sub_path: Option<&str>,
1389 access: Access,
1390) -> io::Result<(Arc<Dir>, Vec<FileIdentity>)> {
1391 let mut ancestors = vec![dir_identity(&dir, Path::new("."), &dir.dir_metadata()?)?];
1392 let Some(path) = sub_path else {
1393 return Ok((dir, ancestors));
1394 };
1395 if path.is_empty()
1396 || path.len() > 4096
1397 || path.split('/').any(|part| {
1398 part.is_empty() || part == "." || part == ".." || part.contains(['\\', '\0'])
1399 })
1400 {
1401 return Err(io::Error::new(
1402 io::ErrorKind::InvalidInput,
1403 "subPath must be a normalized relative path",
1404 ));
1405 }
1406 for component in path.split('/') {
1407 let child = match dir.open_dir_nofollow(component) {
1408 Ok(child) => child,
1409 Err(error)
1410 if error.kind() == io::ErrorKind::NotFound && access == Access::ReadWrite =>
1411 {
1412 match dir.create_dir(component) {
1413 Ok(()) => {}
1414 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1415 Err(error) => return Err(error),
1416 }
1417 dir.open_dir_nofollow(component)?
1418 }
1419 Err(error) => return Err(error),
1420 };
1421 ancestors.push(dir_identity(
1422 &child,
1423 Path::new("."),
1424 &child.dir_metadata()?,
1425 )?);
1426 dir = Arc::new(child);
1427 }
1428 Ok((dir, ancestors))
1429}
1430
1431fn open_root(root: &Path) -> io::Result<Arc<Dir>> {
1432 Dir::open_ambient_dir(root, ambient_authority())
1433 .map(Arc::new)
1434 .map_err(|e| io::Error::new(e.kind(), format!("VFS root {}: {e}", root.display())))
1435}
1436
1437#[cfg(test)]
1438mod tests {
1439 use super::*;
1440
1441 #[test]
1442 fn subpath_creation_and_readonly_cwd() {
1443 let volume = tempfile::tempdir().unwrap();
1444 let vfs = Vfs::external(volume.path().into())
1445 .unwrap()
1446 .with_subpath(Some("users/ada"))
1447 .unwrap();
1448 assert!(volume.path().join("users/ada").is_dir());
1449 let vfs = vfs.with_cwd("/notes/drafts").unwrap();
1450 assert_eq!(vfs.cwd(), "/notes/drafts");
1451 assert!(volume.path().join("users/ada/notes/drafts").is_dir());
1452 let readonly = vfs.with_access(Access::ReadOnly);
1453 assert!(readonly.clone().with_cwd("/notes").is_ok());
1454 assert!(readonly.with_cwd("/missing").is_err());
1455 assert!(
1456 Vfs::external(volume.path().into())
1457 .unwrap()
1458 .with_access(Access::ReadOnly)
1459 .with_subpath(Some("absent"))
1460 .is_err()
1461 );
1462 }
1463
1464 #[cfg(unix)]
1465 #[test]
1466 fn subpath_selection_rejects_symlinks() {
1467 let volume = tempfile::tempdir().unwrap();
1468 std::fs::create_dir_all(volume.path().join("users/ada")).unwrap();
1469 std::os::unix::fs::symlink("ada", volume.path().join("users/alias")).unwrap();
1470 let vfs = Vfs::external(volume.path().into()).unwrap();
1471 assert!(vfs.with_subpath(Some("users/alias")).is_err());
1472 }
1473
1474 #[test]
1475 fn tempdir_is_deleted_on_drop() {
1476 let vfs = Vfs::tempdir().expect("tempdir");
1477 let path = vfs.root().to_path_buf();
1478 assert!(path.is_dir());
1479 drop(vfs);
1480 assert!(!path.exists(), "tempdir should be deleted on drop");
1481 }
1482
1483 #[test]
1484 fn external_does_not_delete_directory() {
1485 let outer = tempfile::tempdir().expect("outer tempdir");
1486 let path = outer.path().to_path_buf();
1487 let vfs = Vfs::external(path.clone()).expect("external");
1488 drop(vfs);
1489 assert!(path.is_dir(), "external path must survive Vfs drop");
1490 }
1491
1492 #[test]
1493 fn external_rejects_missing_path() {
1494 let outer = tempfile::tempdir().expect("outer tempdir");
1495 let missing = outer.path().join("does-not-exist");
1496 let err = Vfs::external(missing).expect_err("must reject missing path");
1497 assert!(
1498 err.to_string().contains("VFS root"),
1499 "error should mention VFS root: {err}"
1500 );
1501 }
1502
1503 #[test]
1504 fn real_directory_yields_a_usable_handle() {
1505 let outer = tempfile::tempdir().expect("outer tempdir");
1506 std::fs::write(outer.path().join("a.txt"), b"hi").expect("write");
1507 let vfs = Vfs::external(outer.path().to_path_buf()).expect("external");
1508 let dir = vfs.dir().expect("an external root backs a directory");
1509 assert_eq!(dir.read("a.txt").expect("read through handle"), b"hi");
1510 }
1511
1512 #[test]
1513 fn none_backs_no_handle() {
1514 let vfs = Vfs::none();
1515 assert!(vfs.dir().is_none());
1516 assert_eq!(vfs.mode(), VfsMode::None);
1517 }
1518
1519 #[test]
1520 fn tempdir_modes_carry_a_handle() {
1521 let vfs = Vfs::tempdir().expect("tempdir");
1522 assert!(
1523 vfs.dir().is_some(),
1524 "ephemeral roots resolve through a handle too"
1525 );
1526 }
1527
1528 #[test]
1529 fn handle_survives_a_rename_of_the_root() {
1530 let outer = tempfile::tempdir().expect("outer tempdir");
1531 let from = outer.path().join("before");
1532 let to = outer.path().join("after");
1533 std::fs::create_dir(&from).expect("mkdir");
1534 std::fs::write(from.join("a.txt"), b"hi").expect("write");
1535
1536 let vfs = Vfs::external(from.clone()).expect("external");
1537 std::fs::rename(&from, &to).expect("rename");
1538
1539 let dir = vfs.dir().expect("handle");
1542 assert_eq!(dir.read("a.txt").expect("read after rename"), b"hi");
1543 }
1544
1545 #[test]
1546 fn external_rejects_file() {
1547 let outer = tempfile::tempdir().expect("outer tempdir");
1548 let file = outer.path().join("a.txt");
1549 std::fs::write(&file, b"hi").expect("write");
1550 let err = Vfs::external(file).expect_err("must reject file");
1551 assert!(
1552 err.to_string().contains("not a directory"),
1553 "error should mention not a directory: {err}"
1554 );
1555 }
1556
1557 #[test]
1558 fn size_limit_starts_from_what_the_directory_holds() {
1559 let outer = tempfile::tempdir().expect("outer tempdir");
1560 std::fs::write(outer.path().join("a.txt"), vec![0u8; 300]).expect("write");
1561 std::fs::create_dir(outer.path().join("sub")).expect("mkdir");
1562 std::fs::write(outer.path().join("sub/b.txt"), vec![0u8; 200]).expect("write");
1563 #[cfg(unix)]
1564 std::os::unix::fs::symlink("a.txt", outer.path().join("link")).expect("symlink");
1565
1566 let vfs = Vfs::external_with_mode(outer.path().to_path_buf(), VfsMode::PerSession)
1567 .expect("external")
1568 .with_size_limit(1_000);
1569 let quota = vfs.quota().expect("quota attached");
1570 assert_eq!(quota.used(), 500, "links count as nothing");
1571 assert_eq!(quota.limit(), 1_000);
1572 assert!(
1573 vfs.clone().quota().is_some_and(|q| Arc::ptr_eq(q, quota)),
1574 "clones share it"
1575 );
1576 }
1577
1578 #[test]
1579 fn measuring_many_directories_holds_one_open_per_level() {
1580 let outer = tempfile::tempdir().expect("outer tempdir");
1581 let mut path = outer.path().to_path_buf();
1582 for depth in 0..3_000 {
1583 if depth % 60 == 0 {
1584 path = outer.path().join(format!("branch{depth}"));
1585 } else {
1586 path = path.join("d");
1587 }
1588 std::fs::create_dir_all(&path).expect("mkdir");
1589 }
1590 std::fs::write(path.join("leaf.txt"), b"12345").expect("write");
1591 let root = Vfs::external(outer.path().to_path_buf()).expect("external");
1592 assert_eq!(root.measure_usage().expect("measure"), 5);
1593 }
1594
1595 #[test]
1596 fn listing_files_stops_past_its_entry_cap() {
1597 let outer = tempfile::tempdir().expect("outer tempdir");
1598 std::fs::write(outer.path().join("a"), b"1").expect("write");
1599 std::fs::create_dir(outer.path().join("d")).expect("mkdir");
1600 let root = Vfs::external(outer.path().to_path_buf()).expect("external");
1601 let dir = root.dir().expect("dir");
1602 assert_eq!(regular_files(dir, 2).expect("two entries fit").len(), 1);
1603 std::fs::write(outer.path().join("d/b"), b"2").expect("write");
1604 let error = regular_files(dir, 2).expect_err("a third entry passes the cap");
1605 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
1606 }
1607
1608 #[test]
1609 fn a_tree_nested_past_the_measured_depth_opens_as_full() {
1610 let outer = tempfile::tempdir().expect("outer tempdir");
1611 let mut path = outer.path().to_path_buf();
1612 for _ in 0..MAX_MEASURED_DEPTH {
1613 path = path.join("d");
1614 }
1615 std::fs::create_dir_all(&path).expect("mkdir");
1616 let fits = Vfs::external(outer.path().to_path_buf()).expect("external");
1617 assert_eq!(fits.measure_usage().expect("measure"), 0);
1618
1619 std::fs::create_dir(path.join("d")).expect("mkdir");
1620 let vfs = Vfs::external(outer.path().to_path_buf())
1621 .expect("external")
1622 .with_size_limit(1_000);
1623 assert!(vfs.quota().is_some_and(|quota| quota.is_unmeasured()));
1624 }
1625
1626 #[cfg(unix)]
1627 #[test]
1628 fn a_directory_that_cannot_be_read_opens_as_full() {
1629 use std::os::unix::fs::PermissionsExt;
1630 let outer = tempfile::tempdir().expect("outer tempdir");
1631 let locked = outer.path().join("locked");
1632 std::fs::create_dir(&locked).expect("mkdir");
1633 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod");
1634 let vfs = Vfs::external(outer.path().to_path_buf())
1635 .expect("external")
1636 .with_size_limit(1_000);
1637 let unmeasured = vfs.quota().is_some_and(|quota| quota.is_unmeasured());
1638 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755))
1639 .expect("chmod back");
1640 assert!(unmeasured, "a failed walk leaves the VFS treated as full");
1641 }
1642
1643 #[test]
1644 fn only_a_walk_that_skips_vanished_entries_survives_one() {
1645 let gone = || io::Result::<u8>::Err(io::ErrorKind::NotFound.into());
1646 assert!(vanished(gone(), false).is_err());
1648 assert_eq!(vanished(gone(), true).expect("skipped"), None);
1649 let denied = io::Result::<u8>::Err(io::ErrorKind::PermissionDenied.into());
1650 assert!(vanished(denied, true).is_err());
1651 assert_eq!(vanished(Ok(1u8), false).expect("ok"), Some(1));
1652 let dir = tempfile::tempdir().expect("dir");
1653 std::fs::write(dir.path().join("a"), "abc").expect("write");
1654 assert_eq!(measure_host_dir(dir.path()).expect("strict"), 3);
1655 assert_eq!(
1656 measure_host_dir_skipping_vanished(dir.path()).expect("lenient"),
1657 3
1658 );
1659 }
1660
1661 fn copied(from: &Path, cap: u64) -> (tempfile::TempDir, Result<u64, CopyDirError>) {
1662 let to = tempfile::tempdir().expect("to");
1663 let mut bytes = 0;
1664 let result = copy_host_dir(from, &to.path().join("out"), cap, &mut bytes).map(|()| bytes);
1665 (to, result)
1666 }
1667
1668 #[test]
1669 fn a_copy_has_the_files_and_stops_at_the_cap() {
1670 let from = tempfile::tempdir().expect("from");
1671 std::fs::create_dir(from.path().join("sub")).expect("mkdir");
1672 std::fs::write(from.path().join("sub/a.txt"), "hello").expect("write");
1673 let (to, result) = copied(from.path(), 100);
1674 assert_eq!(result.expect("copied"), 5);
1675 assert_eq!(
1676 std::fs::read_to_string(to.path().join("out/sub/a.txt")).expect("read"),
1677 "hello"
1678 );
1679 let (_to, result) = copied(from.path(), 4);
1680 assert!(matches!(result, Err(CopyDirError::OverCap)));
1681 }
1682
1683 #[cfg(unix)]
1684 #[test]
1685 fn a_copy_recreates_links_and_never_follows_them() {
1686 let outside = tempfile::tempdir().expect("outside");
1687 std::fs::write(outside.path().join("secret"), "host file").expect("write");
1688 std::fs::create_dir(outside.path().join("dir")).expect("mkdir");
1689 std::fs::write(outside.path().join("dir/inner"), "host dir file").expect("write");
1690 let from = tempfile::tempdir().expect("from");
1691 std::os::unix::fs::symlink(outside.path().join("secret"), from.path().join("file-link"))
1692 .expect("symlink");
1693 std::os::unix::fs::symlink(outside.path().join("dir"), from.path().join("dir-link"))
1694 .expect("symlink");
1695 let (to, result) = copied(from.path(), 1_000);
1696 assert_eq!(result.expect("copied"), 0, "no linked content is copied");
1697 for name in ["file-link", "dir-link"] {
1698 let copy = to.path().join("out").join(name);
1699 assert!(std::fs::symlink_metadata(©).expect("kept").is_symlink());
1700 }
1701 assert!(!to.path().join("out/dir-link-copy").exists());
1702 }
1703
1704 #[cfg(unix)]
1705 #[test]
1706 fn an_entry_that_is_a_link_when_opened_is_not_followed() {
1707 let outside = tempfile::tempdir().expect("outside");
1708 std::fs::write(outside.path().join("secret"), "host file").expect("write");
1709 std::fs::create_dir(outside.path().join("dir")).expect("mkdir");
1710 let from = tempfile::tempdir().expect("from");
1711 let to = tempfile::tempdir().expect("to");
1712 std::os::unix::fs::symlink(outside.path().join("secret"), from.path().join("swapped"))
1713 .expect("symlink");
1714 std::os::unix::fs::symlink(outside.path().join("dir"), from.path().join("swapped-dir"))
1715 .expect("symlink");
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 copy.entry(&source, &target, OsStr::new("swapped"), EntryKind::File, 0)
1727 .expect("file");
1728 copy.entry(
1729 &source,
1730 &target,
1731 OsStr::new("swapped-dir"),
1732 EntryKind::Dir,
1733 0,
1734 )
1735 .expect("dir");
1736 assert_eq!(bytes, 0);
1737 for name in ["swapped", "swapped-dir"] {
1738 let copy = to.path().join(name);
1739 assert!(std::fs::symlink_metadata(©).expect("kept").is_symlink());
1740 }
1741 }
1742
1743 #[test]
1744 fn an_entry_removed_since_it_was_listed_is_skipped() {
1745 let from = tempfile::tempdir().expect("from");
1746 let to = tempfile::tempdir().expect("to");
1747 let source = Dir::open_ambient_dir(from.path(), ambient_authority()).expect("source");
1748 let target = Dir::open_ambient_dir(to.path(), ambient_authority()).expect("target");
1749 let mut bytes = 0;
1750 let mut copy = TreeCopy {
1751 cap: 1_000,
1752 bytes: &mut bytes,
1753 entries: 0,
1754 rel: PathBuf::new(),
1755 };
1756 for kind in [EntryKind::Dir, EntryKind::File, EntryKind::Link] {
1757 copy.entry(&source, &target, OsStr::new("gone"), kind, 0)
1758 .expect("skipped");
1759 }
1760 assert_eq!(std::fs::read_dir(to.path()).expect("list").count(), 0);
1761 }
1762
1763 #[test]
1764 fn an_entry_that_changed_kind_since_it_was_listed_is_copied_as_what_it_is() {
1765 let from = tempfile::tempdir().expect("from");
1766 let to = tempfile::tempdir().expect("to");
1767 std::fs::write(from.path().join("was-dir"), "abc").expect("write");
1768 std::fs::write(from.path().join("was-link"), "de").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.entry(&source, &target, OsStr::new("was-dir"), EntryKind::Dir, 0)
1779 .expect("dir now a file");
1780 copy.entry(&source, &target, OsStr::new("was-link"), EntryKind::Link, 0)
1781 .expect("link now a file");
1782 copy.entry(&source, &target, OsStr::new("was-dir"), EntryKind::File, 0)
1784 .expect("duplicate");
1785 assert_eq!(bytes, 5);
1786 assert_eq!(
1787 std::fs::read_to_string(to.path().join("was-link")).expect("read"),
1788 "de"
1789 );
1790 }
1791
1792 #[cfg(unix)]
1793 #[test]
1794 fn an_entry_that_changes_kind_a_second_time_is_skipped_not_chased() {
1795 let outside = tempfile::tempdir().expect("outside");
1796 let from = tempfile::tempdir().expect("from");
1797 let to = tempfile::tempdir().expect("to");
1798 std::os::unix::fs::symlink(outside.path(), from.path().join("now-link")).expect("symlink");
1799 std::fs::write(from.path().join("now-file"), "abc").expect("write");
1800 let source = Dir::open_ambient_dir(from.path(), ambient_authority()).expect("source");
1801 let target = Dir::open_ambient_dir(to.path(), ambient_authority()).expect("target");
1802 let mut bytes = 0;
1803 let mut copy = TreeCopy {
1804 cap: 1_000,
1805 bytes: &mut bytes,
1806 entries: 0,
1807 rel: PathBuf::new(),
1808 };
1809 copy.file(&source, &target, OsStr::new("now-link"), false)
1811 .expect("re-classified once");
1812 assert!(
1813 std::fs::symlink_metadata(to.path().join("now-link"))
1814 .expect("kept")
1815 .is_symlink()
1816 );
1817 copy.link(&source, &target, OsStr::new("now-file"), true)
1820 .expect("skipped");
1821 copy.file(&source, &target, OsStr::new("now-link"), true)
1822 .expect("skipped");
1823 assert!(!to.path().join("now-file").exists());
1824 assert_eq!(bytes, 0);
1825 }
1826
1827 #[cfg(unix)]
1828 #[test]
1829 fn an_entry_that_flipped_back_is_skipped_but_an_unreadable_one_still_fails() {
1830 use std::os::unix::fs::PermissionsExt;
1831 let from = tempfile::tempdir().expect("from");
1832 std::fs::write(from.path().join("plain"), "abc").expect("write");
1833 std::fs::create_dir(from.path().join("dir")).expect("mkdir");
1834 let locked = from.path().join("locked");
1835 std::fs::write(&locked, "x").expect("write");
1836 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod");
1837 let source = Dir::open_ambient_dir(from.path(), ambient_authority()).expect("source");
1838 let to = tempfile::tempdir().expect("to");
1839 let target = Dir::open_ambient_dir(to.path(), ambient_authority()).expect("target");
1840 let mut bytes = 0;
1841 let mut copy = TreeCopy {
1842 cap: 1_000,
1843 bytes: &mut bytes,
1844 entries: 0,
1845 rel: PathBuf::new(),
1846 };
1847 copy.entry(&source, &target, OsStr::new("plain"), EntryKind::Dir, 0)
1850 .expect("file listed as dir, re-classified");
1851 let not_a_dir = io::Error::from_raw_os_error(libc::ENOTDIR);
1852 copy.changed(
1853 &source,
1854 &target,
1855 OsStr::new("dir"),
1856 EntryKind::Dir,
1857 not_a_dir,
1858 false,
1859 )
1860 .expect("flipped back to a directory");
1861 let not_a_link = io::Error::from_raw_os_error(libc::EINVAL);
1862 copy.changed(
1863 &source,
1864 &target,
1865 OsStr::new("plain"),
1866 EntryKind::File,
1867 not_a_link,
1868 false,
1869 )
1870 .expect("flipped back to a file");
1871 let loop_error = io::Error::from_raw_os_error(libc::ELOOP);
1872 copy.changed(
1873 &source,
1874 &target,
1875 OsStr::new("plain"),
1876 EntryKind::Link,
1877 loop_error,
1878 true,
1879 )
1880 .expect("reclassified, wrong kind");
1881 if unsafe { libc::geteuid() } != 0 {
1883 let result = copy.file(&source, &target, OsStr::new("locked"), true);
1885 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o600))
1886 .expect("chmod");
1887 let Err(CopyDirError::Io(error)) = result else {
1888 panic!("expected an I/O error, got {result:?}");
1889 };
1890 assert!(error.to_string().starts_with("locked: "), "{error}");
1891 }
1892 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o600)).expect("chmod");
1893 let denied = io::Error::from_raw_os_error(libc::EIO);
1895 let result = copy.changed(
1896 &source,
1897 &target,
1898 OsStr::new("dir"),
1899 EntryKind::Dir,
1900 denied,
1901 false,
1902 );
1903 assert!(matches!(result, Err(CopyDirError::Io(_))));
1904 }
1905
1906 #[cfg(unix)]
1907 #[test]
1908 fn an_unreadable_entry_fails_the_copy_naming_its_path_in_the_tree() {
1909 use std::os::unix::fs::PermissionsExt;
1910 let from = tempfile::tempdir().expect("from");
1911 std::fs::create_dir(from.path().join("sub")).expect("mkdir");
1912 let locked = from.path().join("sub/locked");
1913 std::fs::write(&locked, "x").expect("write");
1914 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod");
1915 let (_to, result) = copied(from.path(), 1_000);
1916 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o600)).expect("chmod");
1917 if unsafe { libc::geteuid() } == 0 {
1919 return;
1920 }
1921 let Err(CopyDirError::Io(error)) = result else {
1922 panic!("expected an I/O error, got {result:?}");
1923 };
1924 let message = error.to_string();
1925 assert!(message.starts_with("sub/locked: "), "{message}");
1926 assert!(!message.contains(from.path().to_str().expect("utf8")));
1927 }
1928
1929 #[test]
1930 fn a_missing_component_opens_as_none() {
1931 let volume = tempfile::tempdir().expect("volume");
1932 std::fs::create_dir_all(volume.path().join("a")).expect("mkdir");
1933 assert!(
1934 open_host_subdir(volume.path(), Path::new("a"))
1935 .unwrap()
1936 .is_some()
1937 );
1938 assert!(
1939 open_host_subdir(volume.path(), Path::new("a/b/c"))
1940 .unwrap()
1941 .is_none()
1942 );
1943 assert!(
1944 open_host_subdir(volume.path(), Path::new("x"))
1945 .unwrap()
1946 .is_none()
1947 );
1948 assert_eq!(
1949 measure_host_subdir_skipping_vanished(volume.path(), Path::new("a/b")).unwrap(),
1950 0
1951 );
1952 }
1953
1954 #[cfg(unix)]
1955 #[test]
1956 fn a_link_component_is_an_error() {
1957 let volume = tempfile::tempdir().expect("volume");
1958 let outside = tempfile::tempdir().expect("outside");
1959 std::fs::create_dir_all(outside.path().join("deep")).expect("mkdir");
1960 std::fs::write(outside.path().join("deep/f"), "x").expect("write");
1961 std::fs::create_dir_all(volume.path().join("a")).expect("mkdir");
1962 std::os::unix::fs::symlink(outside.path(), volume.path().join("a/link")).expect("link");
1963 for sub in ["a/link", "a/link/deep"] {
1964 let error = open_host_subdir(volume.path(), Path::new(sub)).expect_err("refused");
1965 assert!(error.to_string().starts_with(sub), "{error}");
1966 assert!(measure_host_subdir_skipping_vanished(volume.path(), Path::new(sub)).is_err());
1967 let to = tempfile::tempdir().expect("to");
1968 let result = copy_host_subdir(volume.path(), Path::new(sub), to.path(), 1_000, &mut 0);
1969 assert!(matches!(result, Err(CopyDirError::Io(_))));
1970 }
1971 }
1972
1973 #[test]
1974 fn a_sub_path_is_copied_to_the_same_place_under_the_target() {
1975 let from = tempfile::tempdir().expect("from");
1976 std::fs::create_dir_all(from.path().join("users/ada/notes")).expect("mkdir");
1977 std::fs::write(from.path().join("users/ada/notes/n.txt"), "note").expect("write");
1978 std::fs::write(from.path().join("users/ada/a.txt"), "ada").expect("write");
1979 std::fs::write(from.path().join("users/b.txt"), "bob").expect("write");
1980 std::fs::write(from.path().join("top.txt"), "top").expect("write");
1981 let to = tempfile::tempdir().expect("to");
1982 let mut bytes = 0;
1983 copy_host_subdir(
1984 from.path(),
1985 Path::new("users/ada"),
1986 to.path(),
1987 1_000,
1988 &mut bytes,
1989 )
1990 .expect("copied");
1991 assert_eq!(bytes, 7);
1992 let read = |rel: &str| std::fs::read_to_string(to.path().join(rel)).expect(rel);
1993 assert_eq!(read("users/ada/notes/n.txt"), "note");
1994 assert_eq!(read("users/ada/a.txt"), "ada");
1995 assert!(!to.path().join("users/b.txt").exists());
1996 assert!(!to.path().join("top.txt").exists());
1997 assert_eq!(
1998 measure_host_subdir_skipping_vanished(from.path(), Path::new("users/ada")).unwrap(),
1999 7
2000 );
2001 }
2002
2003 #[test]
2004 fn a_copy_stops_in_a_tree_nested_too_deep() {
2005 let from = tempfile::tempdir().expect("from");
2006 let mut path = from.path().to_path_buf();
2007 for _ in 0..=MAX_MEASURED_DEPTH + 1 {
2008 path = path.join("d");
2009 }
2010 std::fs::create_dir_all(&path).expect("mkdir");
2011 let (_to, result) = copied(from.path(), 1_000);
2012 assert!(matches!(result, Err(CopyDirError::TooMany)));
2013 }
2014}