Skip to main content

submilli_engine/runtime/
vfs.rs

1//! VFS root directory plumbing.
2//!
3//! The interpreter is agnostic about *how* a VFS came to exist — it only
4//! cares about the directory on disk that backs it. [`Vfs`] carries that
5//! path and, when the interpreter allocated it, owns cleanup.
6//!
7//! It also carries the capability handle every guest path is opened through.
8//! The root is opened once with ambient authority at construction; from then on
9//! every host function resolves against that handle, so a symlink pointing out of
10//! the root is refused by the kernel rather than checked for.
11//!
12//! A VFS may also carry mounts: other directories (named volumes) grafted at
13//! fixed guest paths below the root. Each has its own handle, access mode and
14//! size limit, and a guest path is routed to exactly one of them by
15//! [`Vfs::locate`] before anything is opened.
16
17use 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/// Which blueprint VFS mode this directory backs. Kept independent of the
32/// `submilli-blueprint` enum so the interpreter doesn't depend on that crate;
33/// the server maps one onto the other.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum VfsMode {
36    None,
37    Ephemeral,
38    PerSession,
39    Named,
40}
41
42/// Whether guest code may change what a volume holds.
43#[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    /// `None` only for [`VfsMode::None`], which backs no directory.
65    dir: Option<Arc<Dir>>,
66    _owned: Option<Arc<TempDir>>,
67    /// The blueprint's `size_limit`, shared by every clone so the git worker and
68    /// the program charge one counter.
69    quota: Option<Arc<DiskQuota>>,
70    access: Access,
71    /// The named volume backing the root, under [`VfsMode::Named`].
72    volume: Option<Arc<str>>,
73    mounts: Arc<[Mount]>,
74}
75
76/// A named volume grafted at a guest path below the root.
77#[derive(Debug, Clone)]
78pub struct Mount {
79    /// The guest path, such as `/memory`.
80    guest: Arc<str>,
81    /// The same path relative to the root, such as `memory`.
82    rel: PathBuf,
83    volume: Arc<str>,
84    dir: Arc<Dir>,
85    /// The host directory `dir` was opened on.
86    host: Arc<Path>,
87    access: Access,
88    quota: Option<Arc<DiskQuota>>,
89    /// The identity and exact name of each directory on the way to the mount
90    /// point in the root, the mount point last. A case-insensitive filesystem
91    /// opens these under other spellings too, some not ASCII at all, so the
92    /// mount-point guard recognizes them by identity rather than by name.
93    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/// A directory in the root on the way to a mount point, or the mount point
128/// itself: what a spelling that reaches it must be named as.
129#[derive(Debug, Clone)]
130pub(crate) struct Placeholder {
131    pub(crate) identity: FileIdentity,
132    /// Its parent, exactly as the mount spells it; `""` for the root.
133    pub(crate) parent: PathBuf,
134    pub(crate) name: OsString,
135}
136
137/// What the server hands [`Vfs::with_mount`]: a volume it has resolved by name.
138pub struct MountSpec {
139    /// Absolute guest path, already validated by the blueprint parser; checked
140    /// again here so a direct embedder cannot build an ambiguous table.
141    pub guest_path: String,
142    pub host: PathBuf,
143    pub volume: String,
144    pub access: Access,
145    /// Shared by every VFS that mounts the same volume, so one limit spans them.
146    pub quota: Option<Arc<DiskQuota>>,
147}
148
149/// The volume a resolved guest path lives in: where writes are charged and
150/// whether they are allowed at all.
151#[derive(Debug, Clone)]
152pub struct Placement {
153    access: Access,
154    quota: Option<Arc<DiskQuota>>,
155    /// The mount's guest path, or `None` for the root volume.
156    mount: Option<Arc<str>>,
157    /// The mounts below this volume; only the root has any. A change to the root
158    /// must not remove, replace or write through one of their mount points.
159    nested: Arc<[Mount]>,
160    /// The host directory the volume's handle was opened on.
161    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    /// The guest path of the volume's mount point: `/` for the root.
178    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    /// The host directory the volume's handle was opened on. Only Git uses it,
200    /// to let gix open a repository in place after checking that the path
201    /// names the directory the handle holds; see `stdlib::git::location`.
202    pub(crate) fn host(&self) -> &Path {
203        &self.host
204    }
205
206    /// Whether two placements are the same volume, so a rename between them
207    /// stays within one directory handle and one size limit.
208    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/// Why a mount could not be added.
218#[derive(Debug)]
219pub enum MountError {
220    /// The VFS has no root directory to mount below (`vfs: none`).
221    RootDisabled,
222    /// The guest path is not an absolute, normalized path.
223    BadPath(String),
224    /// `/` is the root itself.
225    AtRoot,
226    /// The guest path names Git metadata.
227    ProtectedPath(String),
228    /// One mount would sit inside another.
229    Nested {
230        outer: String,
231        inner: String,
232    },
233    /// The same volume is already mounted, or backs the root.
234    DuplicateVolume {
235        volume: String,
236        at: String,
237    },
238    TooMany,
239    /// The mount point exists in the root but is not a plain directory, or
240    /// does not exist in a read-only root, which it may not create.
241    MountPointUnavailable {
242        path: String,
243        reason: &'static str,
244    },
245    /// Preparing the mount point in the root failed.
246    RootIo(io::Error),
247    /// Opening the volume's directory failed. Carries no host path; the
248    /// caller logs that.
249    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
288/// The most mounts one VFS carries; routing a path scans them all. The blueprint
289/// parser caps `vfs.mounts` at the same number (`submilli_blueprint::MAX_MOUNTS`)
290/// so a valid blueprint never reaches this; it holds for direct embedders.
291pub const MAX_MOUNTS: usize = 16;
292
293impl Vfs {
294    /// A VFS that backs no directory. Every `submilli:fs.*` call traps; only
295    /// `fs.info()` works (reporting `mode: none`).
296    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    /// Mount an existing host directory the interpreter does not own. Used for
312    /// `named` roots and for `per_session` dirs owned by the server.
313    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    /// An existing host directory, as `submilli run --vfs` exposes it.
339    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    /// Like [`tempdir`](Self::tempdir) but allocated under `parent` (created if
349    /// absent). Lets the host place ephemeral scratch on a chosen volume.
350    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    /// The host path backing this VFS. Server-side logging and test setup only —
377    /// it is never the thing a guest path is resolved against. Empty for
378    /// [`VfsMode::None`].
379    pub fn root(&self) -> &Path {
380        &self.root
381    }
382
383    /// The capability handle guest paths resolve against, or `None` under
384    /// [`VfsMode::None`].
385    ///
386    /// A handle outlives the directory it was opened on: after the root is deleted
387    /// the handle still answers `is_dir()`, so it is not a liveness check.
388    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    /// Enforce `limit` bytes on the files this VFS holds, starting from what the
397    /// directory holds now; see [`with_measured_limit`](Self::with_measured_limit).
398    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    /// Enforce `limit` bytes, starting from `used`, what a walk of the directory
404    /// found. A directory already over the limit still opens: reads and removals
405    /// work, and writes that grow it are refused until it is back under. One that
406    /// couldn't be measured (`None`) opens the same way, as if full, so a program
407    /// can still remove files and let a later run measure it.
408    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    /// Charge the root to `quota`, a limit the server shares between every VFS
418    /// that opens the same named volume.
419    pub fn with_shared_quota(mut self, quota: Arc<DiskQuota>) -> Self {
420        self.quota = Some(quota);
421        self
422    }
423
424    /// The root's size limit and its running count, when one applies.
425    pub fn quota(&self) -> Option<&Arc<DiskQuota>> {
426        self.quota.as_ref()
427    }
428
429    /// Whether guest code may change the root. Mounts carry their own access.
430    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    /// Record the named volume backing the root, for `fs.info()`.
440    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    /// Graft the volume `spec` describes at its guest path.
454    ///
455    /// In a writable root the mount point is created as an empty directory, so
456    /// listing its parent shows it like any other directory. A read-only root is
457    /// never written, so there the mount point must already exist.
458    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        // Case is folded as the blueprint parser folds it, so two spellings of one
476        // directory on a case-insensitive filesystem cannot both be mounted.
477        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    /// Charge the volume mounted at `guest_path` to `quota`, the limit the
514    /// server shares between every VFS that mounts it. No mount there is a
515    /// no-op.
516    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(&quota));
524                }
525                mount
526            })
527            .collect();
528        self.mounts = Arc::from(mounts);
529        self
530    }
531
532    /// Route a root-relative path (as the lexical pass produced it) to the
533    /// volume that holds it: the handle to open it through, the path relative
534    /// to that handle, and the volume's placement. `None` under
535    /// [`VfsMode::None`].
536    ///
537    /// Matching is by whole components, so `/memoryx` is not under `/memory`.
538    /// A mount point itself resolves to its volume's root, `.`.
539    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            // Git must also avoid publishing inside hidden placeholders.
598            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    /// Guest working directory shared by I/O, policy, and introspection.
609    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    /// Select a directory through the volume handle, without ambient reopening.
635    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    /// The bytes held by regular files under the root; see [`measure_dir`].
650    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
658/// The bytes held by regular files under the host directory `root`, as a size
659/// limit starting from it counts them. The error carries no host path; a tree with too
660/// many entries or nested too deep is [`io::ErrorKind::InvalidData`].
661pub fn measure_host_dir(root: &Path) -> io::Result<u64> {
662    measure_dir(&*open_volume(root)?)
663}
664
665/// As [`measure_host_dir`], but an entry removed mid-walk is not counted instead of
666/// failing the measure: for a tree being copied, never for a size limit's accounting.
667pub 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
676/// As [`measure_host_dir_skipping_vanished`], for the directory `sub` below the volume
677/// `volume`, which is no link all the way down. A `sub` that is not there is empty.
678pub 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
689/// Opens the directory `sub` below the host directory `volume`, one component at a time
690/// without following links, so a link anywhere in `sub` is an error. `None` when a
691/// component is not there. `sub` must be relative with no `.` or `..`.
692pub 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
715/// The bytes held by regular files under `root`; see [`for_each_regular_file`].
716pub 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
724/// Every regular file under `root` and its size, as a removal of the tree frees
725/// them; more than `max_entries` entries is an [`io::ErrorKind::InvalidData`] error.
726pub 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
734/// Visit each regular file under `root`. Links are never followed, so a link cannot
735/// make a file count twice. The walk goes depth first, holding one open directory
736/// per level, so its descriptors and memory grow with depth, not with the tree.
737///
738/// An entry that cannot be read is an error, so a size limit treats the tree as full;
739/// with `skip_vanished` one that was removed meanwhile (`NotFound`) is left out instead.
740fn 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            // Full metadata, which Windows reads through a handle, carries the
781            // file's identity.
782            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
795/// `None` when the entry was removed meanwhile and `skip` is set.
796fn 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/// Why [`copy_host_dir`] stopped.
805#[derive(Debug)]
806pub enum CopyDirError {
807    Io(io::Error),
808    /// The copy went past its byte cap.
809    OverCap,
810    /// More entries or deeper nesting than a walk goes through, as for [`measure_host_dir`].
811    TooMany,
812}
813
814impl From<io::Error> for CopyDirError {
815    fn from(error: io::Error) -> Self {
816        Self::Io(error)
817    }
818}
819
820/// Copies the host directory `from` to `to`, adding the bytes copied to `bytes` and stopping
821/// once they pass `cap`.
822///
823/// Nothing under `from` is followed: every entry is opened relative to its already open
824/// parent without following links, so a program swapping an entry for a link mid-copy
825/// cannot get a file from outside the tree copied. A link is recreated as a link; an entry
826/// removed meanwhile is skipped. Only `from` itself, which the caller vouches for, is
827/// resolved by path.
828pub 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
846/// As [`copy_host_dir`], for the directory `sub` below the volume `volume`, copied to
847/// `to_root/sub`. `sub` is walked without following links, so one that is a link, or
848/// passes through one, is refused. `to_root` is made either way; a `sub` that is not in
849/// `volume` copies nothing.
850pub 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    /// The directory being copied, relative to the root, for errors.
907    rel: PathBuf,
908}
909
910impl TreeCopy<'_> {
911    /// An error naming the entry it came from, relative to the root.
912    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    /// Copies the entry `name` of `source`, which was listed as `kind`. An entry that is
942    /// another kind by the time it is opened is copied as what it is, whatever it was listed as.
943    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            // Listed twice.
976            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    /// `reclassified`: the entry already turned out another kind than it was listed as.
989    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        // A device or pipe swapped in must not block the open, nor become our terminal.
999        #[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            // Listed twice.
1017            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => return Ok(()),
1018            Err(error) => return Err(self.at(name, error)),
1019        };
1020        // One byte past what is left is enough to tell the cap was passed.
1021        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        // The target is kept as written, where `read_link` would refuse one that leaves
1042        // the directory; it is never resolved here.
1043        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            // Listed twice.
1053            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    /// Settles an entry that could not be handled as `listed`: gone is skipped, and one that
1062    /// is now another kind is copied as that, by its type without following links. An entry
1063    /// still of the kind listed fails the copy as unreadable, unless the error says it is not
1064    /// that kind (it flipped back meanwhile), which skips it like a vanished one. An entry is
1065    /// re-classified once: one that changed kind again meanwhile is skipped the same way, so a
1066    /// program flipping it cannot keep the copy at it.
1067    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            // Swapped for a directory since it was listed: skipped like a vanished entry.
1099            (EntryKind::File | EntryKind::Link, EntryKind::Dir) | (_, EntryKind::Other) => Ok(()),
1100            // Still the kind listed: skipped only if the error says it is not that kind, as
1101            // when it flipped back since the check.
1102            _ if wrong_kind(&error) => Ok(()),
1103            _ => Err(self.at(name, error)),
1104        }
1105    }
1106}
1107
1108/// Whether `error` is the one an open or `read_link` gives an entry of another kind than
1109/// asked for: not a directory, a followed link refused, or not a link.
1110fn 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
1125/// How many entries [`for_each_regular_file`] walks before it gives up, so a directory
1126/// crafted to be huge can't stall every run that opens it.
1127const MAX_MEASURED_ENTRIES: usize = 1_000_000;
1128
1129/// How many directories deep below the root [`for_each_regular_file`] descends
1130/// before it gives up, which bounds the directories it holds open at once.
1131pub const MAX_MEASURED_DEPTH: usize = 64;
1132
1133/// The root-relative form of a mount's guest path, refusing anything that is
1134/// not absolute, normalized, and made of ASCII letters, digits, `.`, `_` and
1135/// `-`. ASCII alone is what the case-folded mount-point guard compares
1136/// reliably on hosts whose filesystems fold case. The blueprint parser applies
1137/// the same rule; this repeats it for direct embedders.
1138fn 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        // A trailing dot is dropped by Windows, which would make `memory.` and
1152        // `memory` one directory.
1153        if part.is_empty() || part.ends_with('.') || !plain {
1154            return Err(bad());
1155        }
1156        rel.push(part);
1157    }
1158    Ok(rel)
1159}
1160
1161/// Whether `path` is `prefix` or below it, comparing whole components with
1162/// ASCII case folded.
1163fn 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
1174/// Make sure the mount point is a plain directory in the root, creating it (and
1175/// its parents) only when the root may be written.
1176///
1177/// Returns each directory on the way, the mount point last; see
1178/// [`placeholders_on`].
1179fn 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                    // Another run created it between the check and here.
1220                    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    // Recheck: a racing writer may have swapped a link in after creation.
1228    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
1236/// The identity and exact name of each directory on the way to `rel`, `rel`
1237/// last, taken after the recheck so a directory swapped in before it isn't the
1238/// one recorded. An identity that can't be read fails the mount: without it an
1239/// alias of the mount point would go unrecognized.
1240fn 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
1255/// How an existing directory on the way to a mount point is spelled on disk.
1256enum Spelling {
1257    Exact,
1258    /// Found only through a case-insensitive lookup.
1259    Variant,
1260    /// Too many entries beside it to tell.
1261    Unknown,
1262}
1263
1264/// Whether the directory entry at `path` is spelled exactly so, not merely found
1265/// through a case-insensitive lookup. Recursive listings descend into a mount by
1266/// matching the entry's name, so a case variant standing in as the mount point
1267/// would hide the volume behind the root's own directory.
1268///
1269/// Checked on every host, since a Linux filesystem can fold case too (ext4
1270/// casefold, or a directory shared in from a Mac). Where the other case finds
1271/// nothing, or a different directory, the filesystem distinguishes case here and
1272/// the exact lookup that found `path` settles it; only a case-folding directory
1273/// is scanned for the exact name.
1274fn 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
1320/// How many entries [`spelling_of`] reads beside a mount point before giving
1321/// up, so a huge directory can't slow every run that mounts below it.
1322const 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        // The handle tracks the inode, not the name — which is also why it cannot be
1489        // used as a liveness check for the directory it was opened on.
1490        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        // A size limit's measure fails, so the directory opens as full.
1596        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(&copy).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        // Listed as a file and a directory, as they were before being swapped.
1675        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(&copy).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        // The same name listed again is left as copied.
1732        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        // Listed as a file, it is a link: copied as one, once.
1759        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        // Already re-classified, one that fails again is skipped, not re-classified again:
1767        // here a link that reads as a file, as if it had flipped back.
1768        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        // Listed as a directory, a file, and a link; each is still the kind that now fails
1797        // the open the way a flipped-back entry does.
1798        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        // A privileged user reads it anyway.
1831        if unsafe { libc::geteuid() } != 0 {
1832            // Reclassified: only wrong-kind errors skip; an unreadable file fails.
1833            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        // Other errors on an entry still of the kind listed fail too.
1843        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        // A privileged user reads it anyway.
1867        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}