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