Skip to main content

acyclic_fs/native_mount/
mod.rs

1//! Native Linux FUSE, macOS NFS, and Windows `ProjFS` projection boundary.
2//!
3//! Drivers project one canonical SDK checkout. They own kernel handles and
4//! callback cursors only; filesystem truth, COW state, and publication remain
5//! in `acyclic-fs`.
6
7use crate::kernel::FileMetadata;
8#[cfg(any(target_os = "linux", target_os = "macos"))]
9use crate::kernel::MetadataField;
10use crate::{FileId, MountId, VolumeId};
11use bytes::Bytes;
12use std::fs::{File, OpenOptions};
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15#[cfg(any(target_os = "linux", target_os = "macos"))]
16use std::time::SystemTime;
17#[cfg(target_os = "linux")]
18use std::time::{Duration, Instant};
19use thiserror::Error;
20
21mod adapter;
22pub use adapter::{CheckoutMountSource, SharedCheckout, SharedCheckoutGuard, SharedCheckoutState};
23
24mod view_gate;
25
26mod lazy;
27pub use lazy::LazyMountSource;
28
29mod routed;
30pub use routed::RoutedMountSource;
31
32mod customer;
33pub use customer::{
34    LazyMount, LazyWorkingSet, Mount, MountLifecycleError, MountOptions, MountPublication,
35};
36
37#[cfg(any(target_os = "linux", target_os = "macos"))]
38mod device;
39
40pub use crate::{
41    CaptureError, CaptureOptions, CaptureReceipt, WatchCaptureReceipt, capture_baseline,
42    capture_paths, capture_root_identity, capture_subtree, capture_watch_batch,
43};
44
45mod materialize;
46pub use materialize::{
47    HostPathReplacement, HostPathRestore, MaterializationReceipt, MaterializeError,
48    MaterializeOptions, materialize_checkout, materialize_checkout_host_path,
49    materialize_checkout_path, materialize_checkout_paths, restore_checkout_host_path,
50};
51pub(crate) use materialize::{
52    MaterializeMode, materialize_checkout_paths_with_mode, materialize_checkout_with_mode,
53};
54
55mod publication;
56pub use publication::{seal_checkout, seal_checkout_with_permit};
57
58mod storage_capabilities;
59pub use storage_capabilities::{
60    NativeStorageCapabilities, NativeStorageCapabilityError, probe_native_storage_capabilities,
61};
62
63mod storage_accelerations;
64pub use storage_accelerations::{
65    NativeBlockCloneAccelerationEvidence, NativeSparseAccelerationEvidence,
66    NativeStorageAccelerationError, NativeStorageAccelerationEvidence,
67    probe_native_storage_accelerations,
68};
69
70#[cfg(target_os = "macos")]
71mod darwin_mount;
72#[cfg(target_os = "linux")]
73mod fuse;
74
75#[cfg(any(target_os = "linux", target_os = "macos"))]
76fn metadata_or<T>(field: MetadataField<T>, unavailable: T) -> T {
77    match field {
78        MetadataField::Unavailable => unavailable,
79        MetadataField::Value(value) => value,
80    }
81}
82
83#[cfg(any(target_os = "linux", target_os = "macos"))]
84fn system_time_ns(value: SystemTime) -> Result<i64, i32> {
85    match value.duration_since(SystemTime::UNIX_EPOCH) {
86        Ok(duration) => i64::try_from(duration.as_nanos()).map_err(|_| libc::EOVERFLOW),
87        Err(error) => i64::try_from(error.duration().as_nanos())
88            .map(|nanos| -nanos)
89            .map_err(|_| libc::EOVERFLOW),
90    }
91}
92
93#[cfg(target_os = "windows")]
94mod projfs;
95
96/// Concrete namespace mechanism selected for this binary.
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum NativeMountKind {
99    /// Linux kernel FUSE through `/dev/fuse`.
100    LinuxFuse,
101    /// Self-contained macOS loopback-NFS session.
102    MacOsNfs,
103    /// Windows Projected File System provider.
104    WindowsProjFs,
105}
106
107/// Native-driver isolation required for simultaneous mount sessions.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum NativeMountSessionIsolation {
110    /// The driver safely owns multiple live sessions in one process.
111    SharedProcess,
112    /// Every live session requires a distinct operating-system process.
113    ProcessIsolated,
114}
115
116/// Non-destructive compile-time and host capability facts.
117#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct NativeMountCapabilities {
119    /// Driver compiled for this target, if any.
120    pub kind: Option<NativeMountKind>,
121    /// Whether a live host probe admitted the required device/framework.
122    pub available: bool,
123    /// Whether exact authored effects are observable on this mechanism.
124    /// A provider may still reject cross-root operations it cannot observe.
125    pub writable: bool,
126    /// Whether I/O issued by the provider process itself is observable.
127    ///
128    /// `ProjFS` suppresses provider-process notifications. Embedded callers
129    /// must use the SDK mutation surface for their own writes or place the
130    /// provider in a dedicated host process.
131    pub provider_process_io_observable: bool,
132    /// Required process isolation for simultaneous sessions.
133    pub session_isolation: NativeMountSessionIsolation,
134    /// Stable bounded reason when unavailable.
135    pub unavailable_reason: Option<String>,
136}
137
138/// One exact mount request; cross-volume routing remains in the SDK mount table.
139#[derive(Clone, Debug, Eq, PartialEq)]
140pub struct NativeMountRequest {
141    /// Process-unique mount identity.
142    pub mount_id: MountId,
143    /// Projected volume.
144    pub volume_id: VolumeId,
145    /// Empty, caller-owned destination.
146    pub destination: PathBuf,
147    /// Whether authored effects may be captured.
148    pub writable: bool,
149}
150
151/// Platform-neutral node facts required by kernel callbacks.
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub struct MountNode {
154    /// Stable path-independent identity.
155    pub file_id: FileId,
156    /// Canonical node kind.
157    pub kind: MountNodeKind,
158    /// Logical regular-file length; zero for non-files.
159    pub logical_bytes: u64,
160    /// Exact namespace binding count for this path-independent file.
161    pub link_count: u64,
162    /// Native device identity for character/block devices only.
163    pub device: Option<(u32, u32)>,
164}
165
166/// One exact mounted path result with complete authenticated metadata.
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub struct MountLookup {
169    /// Stable file identity, kind, length, and link count.
170    pub node: MountNode,
171    /// Complete metadata; unavailable fields are never fabricated.
172    pub metadata: FileMetadata,
173}
174
175/// Namespace kinds representable by native projections.
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub enum MountNodeKind {
178    /// Regular byte stream.
179    Regular,
180    /// Directory.
181    Directory,
182    /// Symbolic link.
183    SymbolicLink,
184    /// POSIX named pipe.
185    Fifo,
186    /// POSIX socket node.
187    Socket,
188    /// POSIX character device.
189    CharacterDevice,
190    /// POSIX block device.
191    BlockDevice,
192    /// A canonical kind this driver cannot project safely.
193    Unsupported,
194}
195
196/// Native sparse-file seek target.
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum MountSeekTarget {
199    /// First allocated-zero or content byte.
200    Data,
201    /// First unallocated hole byte, including logical end-of-file.
202    Hole,
203}
204
205/// Exact sparse-allocation operation requested by a native mount.
206#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub enum MountRangeAllocation {
208    /// Deallocates the range without changing logical size.
209    PunchHole,
210    /// Replaces the range with allocated canonical zeros.
211    ZeroRange {
212        /// Permit logical growth to the range end.
213        extend: bool,
214    },
215    /// Allocates only holes while preserving all existing content.
216    Preallocate {
217        /// Preserve logical file length.
218        keep_size: bool,
219    },
220}
221
222/// One data span within a bounded sparse open-file snapshot.
223#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct MountSparseSpan {
225    /// Offset relative to the requested snapshot range.
226    pub relative_offset: u64,
227    /// Exact allocated bytes; holes are omitted.
228    pub bytes: Bytes,
229}
230
231/// Bounded sparse range transferred between path-independent native handles.
232#[derive(Clone, Debug, Eq, PartialEq)]
233pub struct MountSparseRange {
234    /// Logical bytes represented, including omitted holes.
235    pub logical_bytes: u64,
236    /// Ordered, non-overlapping allocated spans.
237    pub spans: Vec<MountSparseSpan>,
238}
239
240/// Path-independent regular-file handle used for every native open operation.
241///
242/// Attached implementations mutate the authenticated checkout by stable
243/// [`FileId`], so rename and unlink of one hard-link name
244/// cannot stale the handle. After final-link removal, drivers replace all
245/// handles for the inode with one shared detached implementation.
246pub trait MountOpenFile: Send + Sync + 'static {
247    /// Returns current identity, size, and metadata for `fstat`-style calls.
248    ///
249    /// # Errors
250    ///
251    /// Returns stale, cancellation, or authenticated engine failures.
252    fn lookup(&self) -> Result<MountLookup, MountSourceError>;
253    /// Reads one exact bounded logical range.
254    ///
255    /// # Errors
256    ///
257    /// Returns range, storage, authentication, cancellation, or work failures.
258    fn read_range(&self, offset: u64, length: u32) -> Result<Bytes, MountSourceError>;
259    /// Reads at most `maximum_bytes`, clipping to EOF in the same coherent view.
260    ///
261    /// # Errors
262    ///
263    /// Returns the same typed failures as [`Self::read_range`].
264    fn read_up_to(&self, offset: u64, maximum_bytes: u32) -> Result<Bytes, MountSourceError>;
265    /// Finds the next sparse data or hole boundary.
266    ///
267    /// # Errors
268    ///
269    /// Returns sparse-tree, storage, authentication, cancellation, or work failures.
270    fn seek(&self, offset: u64, target: MountSeekTarget) -> Result<Option<u64>, MountSourceError>;
271    /// Replaces one exact byte range in the detached file.
272    ///
273    /// # Errors
274    ///
275    /// Returns range, storage, cancellation, allocation, or work failures.
276    fn write_range(&self, offset: u64, bytes: Bytes) -> Result<(), MountSourceError>;
277    /// Changes the detached logical file length.
278    ///
279    /// # Errors
280    ///
281    /// Returns sparse-tree, storage, cancellation, allocation, or work failures.
282    fn resize(&self, logical_bytes: u64) -> Result<(), MountSourceError>;
283    /// Applies one exact sparse allocation operation.
284    ///
285    /// # Errors
286    ///
287    /// Returns unsupported, range, storage, cancellation, allocation, or work failures.
288    fn allocate_range(
289        &self,
290        offset: u64,
291        length: u64,
292        operation: MountRangeAllocation,
293    ) -> Result<(), MountSourceError>;
294    /// Replaces complete metadata and optionally logical size atomically from
295    /// the native handle's perspective.
296    ///
297    /// # Errors
298    ///
299    /// Returns metadata, sparse-tree, storage, cancellation, allocation, or work failures.
300    fn set_attributes(
301        &self,
302        metadata: FileMetadata,
303        logical_bytes: Option<u64>,
304    ) -> Result<(), MountSourceError>;
305    /// Reads one exact native named attribute.
306    ///
307    /// # Errors
308    ///
309    /// Returns absence, unsupported-profile, storage, authentication, or work failures.
310    fn read_attribute(&self, name: &[u8]) -> Result<Option<Bytes>, MountSourceError>;
311    /// Lists one bounded page of native named-attribute names.
312    ///
313    /// # Errors
314    ///
315    /// Returns unsupported-profile, pagination, storage, authentication, or work failures.
316    fn list_attributes(
317        &self,
318        cursor: Option<&[u8]>,
319        maximum_entries: u32,
320    ) -> Result<MountAttributePage, MountSourceError>;
321    /// Inserts or replaces one exact native named attribute.
322    ///
323    /// # Errors
324    ///
325    /// Returns precondition, unsupported-profile, storage, or work failures.
326    fn write_attribute(
327        &self,
328        name: &[u8],
329        value: Bytes,
330        mode: MountAttributeWriteMode,
331    ) -> Result<(), MountSourceError>;
332    /// Removes one exact native named attribute.
333    ///
334    /// # Errors
335    ///
336    /// Returns absence, unsupported-profile, storage, or work failures.
337    fn remove_attribute(&self, name: &[u8]) -> Result<(), MountSourceError>;
338
339    /// Reads one bounded sparse range without materializing holes.
340    ///
341    /// # Errors
342    ///
343    /// Returns stale, range, storage, authentication, cancellation, allocation,
344    /// or bounded-work failures.
345    fn read_sparse_range(
346        &self,
347        offset: u64,
348        length: u32,
349    ) -> Result<MountSparseRange, MountSourceError> {
350        let logical_bytes = u64::from(length);
351        let end = offset.checked_add(logical_bytes).ok_or_else(|| {
352            MountSourceError::Invalid("sparse range overflows the file address space".to_owned())
353        })?;
354        let mut spans = Vec::new();
355        let mut cursor = offset;
356        while cursor < end {
357            let Some(data_offset) = self.seek(cursor, MountSeekTarget::Data)? else {
358                break;
359            };
360            if data_offset >= end {
361                break;
362            }
363            let hole_offset = self
364                .seek(data_offset, MountSeekTarget::Hole)?
365                .unwrap_or(end)
366                .min(end);
367            if hole_offset <= data_offset {
368                return Err(MountSourceError::Engine(
369                    "sparse seek made no forward progress".to_owned(),
370                ));
371            }
372            let span_length = u32::try_from(hole_offset - data_offset).map_err(|_| {
373                MountSourceError::Invalid("sparse span exceeds callback bounds".to_owned())
374            })?;
375            let bytes = self.read_range(data_offset, span_length)?;
376            if bytes.len() != usize::try_from(span_length).unwrap_or(usize::MAX) {
377                return Err(MountSourceError::Stale);
378            }
379            spans.push(MountSparseSpan {
380                relative_offset: data_offset - offset,
381                bytes,
382            });
383            cursor = hole_offset;
384        }
385        Ok(MountSparseRange {
386            logical_bytes,
387            spans,
388        })
389    }
390
391    /// Replaces one range from a sparse snapshot, preserving every hole.
392    ///
393    /// # Errors
394    ///
395    /// Returns stale, range, sparse-tree, storage, cancellation, allocation,
396    /// or bounded-work failures. Native callers treat partial failures as a
397    /// short copy and never report unverified completion.
398    fn write_sparse_range(
399        &self,
400        offset: u64,
401        range: &MountSparseRange,
402    ) -> Result<(), MountSourceError> {
403        let end = offset.checked_add(range.logical_bytes).ok_or_else(|| {
404            MountSourceError::Invalid("sparse range overflows the file address space".to_owned())
405        })?;
406        let current = self.lookup()?.node.logical_bytes;
407        if end > current {
408            self.resize(end)?;
409        }
410        self.allocate_range(offset, range.logical_bytes, MountRangeAllocation::PunchHole)?;
411        for span in &range.spans {
412            let destination = offset.checked_add(span.relative_offset).ok_or_else(|| {
413                MountSourceError::Invalid("sparse span offset overflows".to_owned())
414            })?;
415            self.write_range(destination, span.bytes.clone())?;
416        }
417        Ok(())
418    }
419}
420
421/// One bounded directory item.
422#[derive(Clone, Debug, Eq, PartialEq)]
423pub struct MountDirectoryEntry {
424    /// Exact path-component bytes in the volume profile.
425    pub name: Vec<u8>,
426    /// Stable node facts.
427    pub node: MountNode,
428    /// Complete authenticated metadata for native stat/enumeration projection.
429    pub metadata: FileMetadata,
430}
431
432/// One independently resumable directory-handle page.
433#[derive(Clone, Debug, Eq, PartialEq)]
434pub struct MountDirectoryPage {
435    /// Ordered entries after the supplied opaque cursor.
436    pub entries: Vec<MountDirectoryEntry>,
437    /// Opaque continuation owned by this open directory handle.
438    pub next_cursor: Option<Vec<u8>>,
439}
440
441/// One bounded page of exact native named-attribute names.
442#[derive(Clone, Debug, Eq, PartialEq)]
443pub struct MountAttributePage {
444    /// Strictly ordered native names after the supplied cursor.
445    pub names: Vec<Vec<u8>>,
446    /// Exact last name when another page exists.
447    pub next_cursor: Option<Vec<u8>>,
448}
449
450/// Native named-attribute write precondition.
451#[derive(Clone, Copy, Debug, Eq, PartialEq)]
452pub enum MountAttributeWriteMode {
453    /// Insert or replace.
454    Upsert,
455    /// Require absence.
456    Create,
457    /// Require presence.
458    Replace,
459}
460
461/// Exact platform-native absolute path used only at the kernel callback edge.
462///
463/// Components contain raw Unix name bytes on Linux FUSE/macOS NFS and little-endian
464/// UTF-16 code units on `ProjFS`. The checkout adapter converts them into the
465/// volume's declared logical-name representation without lossy text routing.
466#[derive(Clone, Debug, Eq, Hash, PartialEq)]
467pub struct MountPath {
468    components: Vec<Vec<u8>>,
469}
470
471impl MountPath {
472    /// Returns the volume root.
473    #[must_use]
474    pub const fn root() -> Self {
475        Self {
476            components: Vec::new(),
477        }
478    }
479
480    /// Appends one exact platform-native component.
481    #[must_use]
482    pub fn child(&self, component: Vec<u8>) -> Self {
483        let mut components = self.components.clone();
484        components.push(component);
485        Self { components }
486    }
487
488    /// Borrows exact root-to-leaf components.
489    #[must_use]
490    pub fn components(&self) -> &[Vec<u8>] {
491        &self.components
492    }
493}
494
495/// Errors returned by the canonical checkout callback bridge.
496#[derive(Clone, Debug, Error)]
497pub enum MountSourceError {
498    /// Path is authentically absent.
499    #[error("path is absent")]
500    NotFound,
501    /// Create-only operation found an existing object.
502    #[error("filesystem object already exists")]
503    AlreadyExists,
504    /// Operation is invalid for the resolved kind or profile.
505    #[error("invalid filesystem operation: {0}")]
506    Invalid(String),
507    /// Operation is not exactly observable or representable on this platform.
508    #[error("filesystem operation is unsupported: {0}")]
509    Unsupported(String),
510    /// Canonical engine operation failed.
511    #[error("filesystem engine failure: {0}")]
512    Engine(String),
513    /// This mount or callback was fenced or cancelled.
514    #[error("mount callback is stale or cancelled")]
515    Stale,
516}
517
518/// An owned permit that pins one coherent projected source view.
519///
520/// Native drivers retain this value through every externally visible callback
521/// result. Sources that can rebind must make rebinding wait for outstanding
522/// permits, eliminating check-then-emit races at the kernel boundary.
523pub trait MountViewLease: Send + 'static {}
524
525impl MountViewLease for () {}
526
527/// Synchronous callback surface implemented by an embedded checkout adapter.
528///
529/// Native kernels invoke callbacks on foreign threads, so this intentionally
530/// exposes a blocking boundary. Implementations may enter an async runtime but
531/// must enforce finite work and timeout limits internally.
532pub trait MountFilesystem: Send + Sync + 'static {
533    /// Whether this source can represent native POSIX named attributes.
534    ///
535    /// macOS must know this before mounting: advertising `namedattr` for a
536    /// profile that rejects POSIX attributes turns ordinary metadata probes
537    /// into I/O errors instead of allowing the OS compatibility fallback.
538    fn supports_posix_named_attributes(&self) -> bool {
539        false
540    }
541
542    /// Whether a native handle close is itself a publication boundary.
543    ///
544    /// Coordinated overlays may batch close-visible mutations until their
545    /// explicit lifecycle boundary; direct checkouts publish on close.
546    fn flush_on_handle_close(&self) -> bool {
547        true
548    }
549
550    /// Whether a lookup may observe one coherent source view right now.
551    /// Drivers must retry or fail stale while this is false.
552    fn view_is_stable(&self) -> bool {
553        true
554    }
555
556    /// Returns the current coherent projection view, when the source can
557    /// precisely invalidate cached lookups.
558    ///
559    /// Sources without an epoch return `None`; drivers must then resolve every
560    /// lookup through the source. An epoch may be reused only until this value
561    /// changes.
562    fn view_epoch(&self) -> Option<u64> {
563        None
564    }
565
566    /// Changes only when existing path or handle bindings may be replaced by
567    /// an external source transition. Ordinary mutations may advance
568    /// `view_epoch` to invalidate caches without making a native callback
569    /// stale. Sources without that distinction retain the conservative view
570    /// epoch behavior.
571    fn binding_epoch(&self) -> Option<u64> {
572        self.view_epoch()
573    }
574
575    /// Pins the current coherent projection view for one native callback.
576    ///
577    /// `expected_epoch` binds a continuation to the view in which it began.
578    /// Stable sources need no retained state; rebindable sources override this
579    /// method with an owned synchronization permit.
580    fn acquire_view_lease(
581        &self,
582        expected_epoch: Option<u64>,
583    ) -> Result<Box<dyn MountViewLease>, MountSourceError> {
584        if !self.view_is_stable() || self.view_epoch() != expected_epoch {
585            return Err(MountSourceError::Stale);
586        }
587        Ok(Box::new(()))
588    }
589
590    /// Pins source binding identity without treating ordinary native writes
591    /// as an external rebind. A callback must separately validate its cache
592    /// epoch before reusing a lookup result.
593    fn acquire_binding_lease(
594        &self,
595        expected_epoch: Option<u64>,
596    ) -> Result<Box<dyn MountViewLease>, MountSourceError> {
597        if !self.view_is_stable() || self.binding_epoch() != expected_epoch {
598            return Err(MountSourceError::Stale);
599        }
600        Ok(Box::new(()))
601    }
602
603    /// Looks up one absolute portable path without following links.
604    ///
605    /// # Errors
606    ///
607    /// Returns a typed source failure without changing checkout state.
608    fn lookup(&self, path: &MountPath) -> Result<Option<MountLookup>, MountSourceError>;
609    /// Opens one regular file as an attached path-independent handle.
610    ///
611    /// # Errors
612    ///
613    /// Returns absence, non-regular-kind, storage, authentication,
614    /// cancellation, or bounded-work failures.
615    fn open_file(&self, path: &MountPath) -> Result<Arc<dyn MountOpenFile>, MountSourceError>;
616    /// Captures one regular file as a detached path-independent open-file view.
617    ///
618    /// The driver calls this immediately before removing the final namespace
619    /// binding while one or more native handles remain open.
620    ///
621    /// # Errors
622    ///
623    /// Returns absence, non-regular-kind, storage, authentication,
624    /// cancellation, or bounded-work failures.
625    fn detach_file(&self, path: &MountPath) -> Result<Arc<dyn MountOpenFile>, MountSourceError>;
626    /// Reads one symbolic link's exact opaque target bytes without following it.
627    ///
628    /// # Errors
629    ///
630    /// Returns a typed source failure when the path is absent or is not a link.
631    fn read_link(&self, path: &MountPath) -> Result<Bytes, MountSourceError>;
632    /// Reads at most one exact logical range.
633    ///
634    /// # Errors
635    ///
636    /// Returns a typed source failure without partial output.
637    fn read_range(
638        &self,
639        path: &MountPath,
640        offset: u64,
641        length: u32,
642    ) -> Result<Bytes, MountSourceError>;
643    /// Finds the first sparse data or hole byte at/after one offset.
644    ///
645    /// # Errors
646    ///
647    /// Returns a typed source failure; `None` means no matching byte exists.
648    fn seek(
649        &self,
650        path: &MountPath,
651        offset: u64,
652        target: MountSeekTarget,
653    ) -> Result<Option<u64>, MountSourceError>;
654    /// Reads one bounded page using a handle-owned opaque cursor.
655    ///
656    /// # Errors
657    ///
658    /// Returns a typed source failure without advancing the cursor.
659    fn read_directory(
660        &self,
661        path: &MountPath,
662        cursor: Option<&[u8]>,
663        maximum_entries: u32,
664    ) -> Result<MountDirectoryPage, MountSourceError>;
665    /// Creates one empty regular file.
666    ///
667    /// # Errors
668    ///
669    /// Returns a typed source failure without partial mutation.
670    fn create_file(
671        &self,
672        path: &MountPath,
673        metadata: FileMetadata,
674    ) -> Result<MountLookup, MountSourceError>;
675    /// Creates one empty directory.
676    ///
677    /// # Errors
678    ///
679    /// Returns a typed source failure without partial mutation.
680    fn create_directory(
681        &self,
682        path: &MountPath,
683        metadata: FileMetadata,
684    ) -> Result<MountLookup, MountSourceError>;
685    /// Creates one symbolic link with an exact opaque target.
686    ///
687    /// # Errors
688    ///
689    /// Returns a typed source failure without partial mutation.
690    fn create_symbolic_link(
691        &self,
692        path: &MountPath,
693        target: Bytes,
694        metadata: FileMetadata,
695    ) -> Result<MountLookup, MountSourceError>;
696    /// Creates one FIFO, socket, character device, or block device.
697    ///
698    /// # Errors
699    ///
700    /// Returns a typed source failure without partial mutation.
701    fn create_special(
702        &self,
703        path: &MountPath,
704        kind: MountNodeKind,
705        device: Option<(u32, u32)>,
706        metadata: FileMetadata,
707    ) -> Result<MountLookup, MountSourceError>;
708    /// Atomically replaces complete metadata and optionally logical size.
709    ///
710    /// # Errors
711    ///
712    /// Returns a typed source failure without partial mutation.
713    fn set_attributes(
714        &self,
715        path: &MountPath,
716        metadata: FileMetadata,
717        logical_bytes: Option<u64>,
718    ) -> Result<(), MountSourceError>;
719    /// Reads one exact native named attribute.
720    ///
721    /// # Errors
722    ///
723    /// Returns typed absence, unsupported-profile, or engine failures.
724    fn read_attribute(
725        &self,
726        path: &MountPath,
727        name: &[u8],
728    ) -> Result<Option<Bytes>, MountSourceError>;
729    /// Lists one bounded page of native named-attribute names.
730    ///
731    /// # Errors
732    ///
733    /// Returns a typed failure without advancing the supplied cursor.
734    fn list_attributes(
735        &self,
736        path: &MountPath,
737        cursor: Option<&[u8]>,
738        maximum_entries: u32,
739    ) -> Result<MountAttributePage, MountSourceError>;
740    /// Inserts or replaces one exact native named attribute.
741    ///
742    /// # Errors
743    ///
744    /// Returns a typed failure without partial mutation.
745    fn write_attribute(
746        &self,
747        path: &MountPath,
748        name: &[u8],
749        value: Bytes,
750        mode: MountAttributeWriteMode,
751    ) -> Result<(), MountSourceError>;
752    /// Removes one exact native named attribute.
753    ///
754    /// # Errors
755    ///
756    /// Returns typed absence or engine failure without partial mutation.
757    fn remove_attribute(&self, path: &MountPath, name: &[u8]) -> Result<(), MountSourceError>;
758    /// Replaces one exact file range.
759    ///
760    /// # Errors
761    ///
762    /// Returns a typed source failure without partial mutation.
763    fn write_range(
764        &self,
765        path: &MountPath,
766        offset: u64,
767        bytes: Bytes,
768    ) -> Result<(), MountSourceError>;
769    /// Changes one file's logical length.
770    ///
771    /// # Errors
772    ///
773    /// Returns a typed source failure without partial mutation.
774    fn resize(&self, path: &MountPath, logical_bytes: u64) -> Result<(), MountSourceError>;
775    /// Applies one exact sparse allocation operation.
776    ///
777    /// # Errors
778    ///
779    /// Returns a typed failure without partial mutation.
780    fn allocate_range(
781        &self,
782        path: &MountPath,
783        offset: u64,
784        length: u64,
785        operation: MountRangeAllocation,
786    ) -> Result<(), MountSourceError>;
787    /// Clones one immutable range between regular files without reading bytes.
788    ///
789    /// # Errors
790    ///
791    /// Returns a typed failure without partial mutation.
792    fn clone_range(
793        &self,
794        source: &MountPath,
795        source_offset: u64,
796        destination: &MountPath,
797        destination_offset: u64,
798        length: u64,
799    ) -> Result<(), MountSourceError>;
800    /// Clones one logical range between stable attached file identities.
801    ///
802    /// # Errors
803    ///
804    /// Returns stale identity, sparse-tree, storage, cancellation, allocation,
805    /// or bounded-work failures. Detached identities are rejected.
806    #[allow(clippy::too_many_arguments)]
807    fn clone_range_by_id(
808        &self,
809        source_file_id: FileId,
810        source_offset: u64,
811        destination_file_id: FileId,
812        destination_offset: u64,
813        length: u64,
814    ) -> Result<(), MountSourceError>;
815    /// Removes one namespace binding.
816    ///
817    /// # Errors
818    ///
819    /// Returns a typed source failure without partial mutation.
820    fn remove(&self, path: &MountPath, expected: Option<FileId>) -> Result<(), MountSourceError>;
821    /// Renames one binding atomically within the volume.
822    ///
823    /// # Errors
824    ///
825    /// Returns a typed source failure without partial mutation.
826    fn rename(
827        &self,
828        source: &MountPath,
829        destination: &MountPath,
830        replace: bool,
831    ) -> Result<(), MountSourceError>;
832    /// Creates one same-volume hard link.
833    ///
834    /// # Errors
835    ///
836    /// Returns a typed source failure without partial mutation.
837    fn hard_link(
838        &self,
839        source: &MountPath,
840        destination: &MountPath,
841    ) -> Result<(), MountSourceError>;
842    /// Seals all pending authored mutations through the checkout's configured
843    /// fenced publication path before acknowledging native durability.
844    ///
845    /// # Errors
846    ///
847    /// Returns stale or engine failure when authority cannot acknowledge the
848    /// exact candidate generation.
849    fn flush(&self) -> Result<(), MountSourceError>;
850    /// Reconciles exact final host state for one projected path and seals it.
851    ///
852    /// This is used by native mechanisms such as `ProjFS` that report an exact
853    /// close-after-modification boundary but do not expose write byte ranges.
854    ///
855    /// # Errors
856    ///
857    /// Returns a typed capture or publication failure without acknowledging
858    /// the native notification.
859    fn capture_host_path(
860        &self,
861        source_root: &Path,
862        path: &MountPath,
863    ) -> Result<(), MountSourceError>;
864    /// Reconciles exact final host states together, preserving hard-link
865    /// topology and admitting one bounded checkout candidate.
866    ///
867    /// # Errors
868    ///
869    /// Returns a typed capture or publication failure without a partial
870    /// checkout candidate.
871    fn capture_host_paths(
872        &self,
873        source_root: &Path,
874        paths: &[MountPath],
875    ) -> Result<(), MountSourceError>;
876    /// Reconciles one imported directory and every descendant atomically.
877    ///
878    /// # Errors
879    ///
880    /// Returns a typed bounded-capture or publication failure.
881    fn capture_host_subtree(
882        &self,
883        source_root: &Path,
884        path: &MountPath,
885    ) -> Result<(), MountSourceError>;
886}
887
888/// Native mount admission and lifecycle failures.
889#[derive(Debug, Error)]
890pub enum NativeMountError {
891    /// This target has no supported native namespace mechanism.
892    #[error("native mounts are unsupported on this target")]
893    UnsupportedTarget,
894    /// Required host driver/device is unavailable.
895    #[error("native mount capability is unavailable: {0}")]
896    CapabilityUnavailable(String),
897    /// Destination must already exist and be empty.
898    #[error("mount destination must be an existing empty directory")]
899    InvalidDestination,
900    /// Another process owns this exact native destination.
901    #[error("mount destination is already owned by another process")]
902    DestinationBusy,
903    /// Requested writable semantics cannot be captured exactly.
904    #[error("writable native mount is unavailable: {0}")]
905    WritableUnavailable(String),
906    /// Volume name representation cannot be projected exactly on this target.
907    #[error("native mount cannot represent the {profile} volume profile on {platform}")]
908    ProfileUnavailable {
909        /// Declared volume profile.
910        profile: &'static str,
911        /// Current native target.
912        platform: &'static str,
913    },
914    /// Platform driver operation failed.
915    #[error("native mount driver failed: {0}")]
916    Driver(String),
917}
918
919enum DriverSession {
920    #[cfg(target_os = "linux")]
921    Fuse(fuse::FuseSession),
922    #[cfg(target_os = "macos")]
923    DarwinMount(darwin_mount::DarwinMountSession),
924    #[cfg(target_os = "windows")]
925    ProjFs(projfs::ProjFsSession),
926    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
927    Unsupported,
928}
929
930struct DriverStartFailure {
931    error: NativeMountError,
932    preserve_destination_fence: bool,
933}
934
935impl DriverStartFailure {
936    #[cfg(any(target_os = "windows", target_os = "macos"))]
937    fn preserving_destination_fence(error: NativeMountError) -> Self {
938        Self {
939            error,
940            preserve_destination_fence: true,
941        }
942    }
943}
944
945impl From<NativeMountError> for DriverStartFailure {
946    fn from(error: NativeMountError) -> Self {
947        Self {
948            error,
949            preserve_destination_fence: false,
950        }
951    }
952}
953
954/// Process-owned mounted namespace. Drop performs one idempotent stop.
955pub struct NativeMountSession {
956    mount_id: MountId,
957    destination: PathBuf,
958    driver: Option<DriverSession>,
959    destination_guard: Option<MountDestinationGuard>,
960}
961
962impl NativeMountSession {
963    /// Stable mount identity.
964    #[must_use]
965    pub const fn mount_id(&self) -> MountId {
966        self.mount_id
967    }
968
969    /// Exact mounted destination.
970    #[must_use]
971    pub fn destination(&self) -> &Path {
972        &self.destination
973    }
974
975    pub(crate) fn flush_callbacks(&self) -> Result<(), NativeMountError> {
976        match self.driver.as_ref() {
977            #[cfg(target_os = "windows")]
978            Some(DriverSession::ProjFs(session)) => session.flush_callbacks(),
979            #[cfg(any(target_os = "linux", target_os = "macos"))]
980            Some(_) => Ok(()),
981            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
982            Some(DriverSession::Unsupported) => Err(NativeMountError::UnsupportedTarget),
983            None => Ok(()),
984        }
985    }
986
987    /// Drops the kernel's cached entry/attributes for one mount-relative
988    /// path (leading `/` optional). Linux FUSE also invalidates resident
989    /// file data. This makes a projection change — such as a
990    /// removed route — visible immediately instead of after a cache timeout.
991    /// Linux FUSE supports nested entries when the parent directory has a
992    /// cached inode; otherwise the entry becomes visible at cache expiry.
993    ///
994    /// # Errors
995    ///
996    /// Returns a driver error when the transport cannot invalidate (the
997    /// change then becomes visible at the cache's own expiry).
998    pub fn invalidate(&self, path: &[u8]) -> Result<(), NativeMountError> {
999        match &self.driver {
1000            #[cfg(target_os = "linux")]
1001            Some(DriverSession::Fuse(session)) => session.invalidate(path),
1002            #[cfg(target_os = "macos")]
1003            Some(DriverSession::DarwinMount(session)) => session.invalidate(path),
1004            #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1005            Some(_) => {
1006                let _ = path;
1007                Err(NativeMountError::Driver(
1008                    "invalidation is not implemented for this transport".to_owned(),
1009                ))
1010            }
1011            None => Err(NativeMountError::Driver("session is stopped".to_owned())),
1012        }
1013    }
1014
1015    /// Detaches the kernel namespace and ends the driver session; returns
1016    /// whether a live session was actually stopped.
1017    ///
1018    /// # Errors
1019    ///
1020    /// Returns a platform error if the kernel namespace cannot be detached.
1021    pub fn stop(&mut self) -> Result<bool, NativeMountError> {
1022        stop_owned_session(
1023            &mut self.driver,
1024            &mut self.destination_guard,
1025            stop_driver,
1026            MountDestinationGuard::release,
1027        )
1028    }
1029}
1030
1031fn stop_owned_session<D, G, E>(
1032    driver: &mut Option<D>,
1033    destination_guard: &mut Option<G>,
1034    mut stop: impl FnMut(&mut D) -> Result<(), E>,
1035    mut release: impl FnMut(&mut G) -> Result<(), E>,
1036) -> Result<bool, E> {
1037    if driver.is_none() && destination_guard.is_none() {
1038        return Ok(false);
1039    }
1040    if let Some(active) = driver.as_mut() {
1041        stop(active)?;
1042        driver.take();
1043    }
1044    if let Some(guard) = destination_guard.as_mut() {
1045        release(guard)?;
1046        destination_guard.take();
1047    }
1048    Ok(true)
1049}
1050
1051fn drop_owned_session<D, G, E>(
1052    driver: &mut Option<D>,
1053    destination_guard: &mut Option<G>,
1054    stop: impl FnMut(&mut D) -> Result<(), E>,
1055    release: impl FnMut(&mut G) -> Result<(), E>,
1056) {
1057    if stop_owned_session(driver, destination_guard, stop, release).is_err() {
1058        // A failed final detach has unknown kernel state. Keep both the driver owner and its
1059        // destination fence alive until process exit instead of publishing the destination as
1060        // reusable while callbacks or a kernel namespace may still exist.
1061        if let Some(active) = driver.take() {
1062            std::mem::forget(active);
1063        }
1064        if let Some(guard) = destination_guard.take() {
1065            std::mem::forget(guard);
1066        }
1067    }
1068}
1069
1070impl Drop for NativeMountSession {
1071    fn drop(&mut self) {
1072        drop_owned_session(
1073            &mut self.driver,
1074            &mut self.destination_guard,
1075            stop_driver,
1076            MountDestinationGuard::release,
1077        );
1078    }
1079}
1080
1081/// Probes the target's one canonical native namespace mechanism.
1082#[must_use]
1083pub fn probe_native_mount() -> NativeMountCapabilities {
1084    platform::probe()
1085}
1086
1087/// Starts a real native projection over the supplied canonical checkout.
1088///
1089/// # Errors
1090///
1091/// Rejects absent capabilities, non-empty destinations, unsupported writable
1092/// capture, or any platform-driver startup failure.
1093pub fn mount_native(
1094    request: NativeMountRequest,
1095    source: Arc<dyn MountFilesystem>,
1096) -> Result<NativeMountSession, NativeMountError> {
1097    let capabilities = probe_native_mount();
1098    if !capabilities.available {
1099        return Err(NativeMountError::CapabilityUnavailable(
1100            capabilities
1101                .unavailable_reason
1102                .unwrap_or_else(|| "unavailable".to_owned()),
1103        ));
1104    }
1105    if request.writable && !capabilities.writable {
1106        return Err(NativeMountError::WritableUnavailable(
1107            "the selected kernel API cannot prove exact authored effects".to_owned(),
1108        ));
1109    }
1110    let (driver, destination_guard) =
1111        start_owned_session(&request.destination, || start_driver(&request, source))?;
1112    Ok(NativeMountSession {
1113        mount_id: request.mount_id,
1114        destination: request.destination,
1115        driver: Some(driver),
1116        destination_guard: Some(destination_guard),
1117    })
1118}
1119
1120/// Starts a native projection mounted directly over an existing,
1121/// non-empty directory — its contents are shadowed for the mount's
1122/// duration and reappear once the returned session stops. Windows `ProjFS`
1123/// cannot shadow local entries and this operation is unavailable there.
1124///
1125/// For callers that must project a checkout at a specific real path that
1126/// already has content (for example, redirecting an entire tool session
1127/// into a fork at the project's own root), as opposed to [`mount_native`]'s
1128/// ordinary empty-destination contract. Process-fencing against concurrent
1129/// mounts of the same destination is identical to `mount_native`.
1130///
1131/// # Errors
1132///
1133/// Same as [`mount_native`], except the destination may be non-empty; it
1134/// must still be an existing, non-symlink directory not already admitted
1135/// by another live mount. Windows returns `WritableUnavailable` because
1136/// `ProjFS` cannot shadow local entries.
1137pub fn mount_native_over_existing(
1138    request: NativeMountRequest,
1139    source: Arc<dyn MountFilesystem>,
1140) -> Result<NativeMountSession, NativeMountError> {
1141    #[cfg(windows)]
1142    {
1143        let _ = (request, source);
1144        Err(NativeMountError::WritableUnavailable(
1145            "Windows ProjFS cannot shadow an existing directory".to_owned(),
1146        ))
1147    }
1148    #[cfg(not(windows))]
1149    {
1150        let capabilities = probe_native_mount();
1151        if !capabilities.available {
1152            return Err(NativeMountError::CapabilityUnavailable(
1153                capabilities
1154                    .unavailable_reason
1155                    .unwrap_or_else(|| "unavailable".to_owned()),
1156            ));
1157        }
1158        if request.writable && !capabilities.writable {
1159            return Err(NativeMountError::WritableUnavailable(
1160                "the selected kernel API cannot prove exact authored effects".to_owned(),
1161            ));
1162        }
1163        let (driver, destination_guard) =
1164            start_owned_session_over_existing(&request.destination, || {
1165                start_driver(&request, source)
1166            })?;
1167        Ok(NativeMountSession {
1168            mount_id: request.mount_id,
1169            destination: request.destination,
1170            driver: Some(driver),
1171            destination_guard: Some(destination_guard),
1172        })
1173    }
1174}
1175
1176/// Reclaims the process fence for one externally detached destination.
1177///
1178/// This is the supervisor-side crash-recovery boundary. A live owner keeps the
1179/// fence locked and causes [`NativeMountError::DestinationBusy`]; an unlocked
1180/// crash-left fence is acquired and removed under the parent registry lock.
1181/// Reclaiming an already-removed fence succeeds idempotently.
1182/// The caller remains responsible for proving that the kernel namespace has
1183/// already detached before invoking this operation.
1184///
1185/// # Errors
1186///
1187/// Rejects a missing or non-canonical destination, a live owner, unsafe fence
1188/// metadata, or a failure to remove the reclaimed fence.
1189pub fn reclaim_native_mount_destination_fence(destination: &Path) -> Result<(), NativeMountError> {
1190    let canonical = destination
1191        .canonicalize()
1192        .map_err(|_| NativeMountError::InvalidDestination)?;
1193    MountDestinationGuard::reclaim_canonical(&canonical)
1194}
1195
1196/// Reclaims every unlocked native-mount destination fence in one parent.
1197///
1198/// The parent registry is held for the complete bounded scan, so a destination
1199/// cannot be admitted while its persistent fence is being classified or
1200/// removed. Fences held by live providers remain untouched. Malformed reserved
1201/// names, links, special files, or an excessive directory fail closed.
1202///
1203/// # Errors
1204///
1205/// Returns a typed destination or driver error when the parent is invalid, the
1206/// reserved fence namespace is unsafe, or the bounded scan cannot complete.
1207pub fn reclaim_stale_native_mount_destination_fences(
1208    parent: &Path,
1209) -> Result<u32, NativeMountError> {
1210    const MAXIMUM_PARENT_ENTRIES: usize = 4_096;
1211    const PREFIX: &str = ".acyclic-fs-mount-";
1212    const SUFFIX: &str = ".lock";
1213
1214    let parent = parent
1215        .canonicalize()
1216        .map_err(|_| NativeMountError::InvalidDestination)?;
1217    let metadata =
1218        std::fs::symlink_metadata(&parent).map_err(|_| NativeMountError::InvalidDestination)?;
1219    if !metadata.is_dir() || metadata.file_type().is_symlink() {
1220        return Err(NativeMountError::InvalidDestination);
1221    }
1222    let registry_path = parent.join(".acyclic-fs-mount-registry.lock");
1223    let registry = acquire_mount_lock(&registry_path, true)?;
1224    let mut reclaimed = 0_u32;
1225    for (index, entry) in std::fs::read_dir(&parent)
1226        .map_err(|error| NativeMountError::Driver(error.to_string()))?
1227        .enumerate()
1228    {
1229        if index >= MAXIMUM_PARENT_ENTRIES {
1230            return Err(NativeMountError::Driver(
1231                "native mount parent exceeds its bounded entry count".to_owned(),
1232            ));
1233        }
1234        let entry = entry.map_err(|error| NativeMountError::Driver(error.to_string()))?;
1235        let name = entry.file_name();
1236        let Some(name) = name.to_str() else {
1237            continue;
1238        };
1239        if name == ".acyclic-fs-mount-registry.lock" || !name.starts_with(PREFIX) {
1240            continue;
1241        }
1242        let digest = name
1243            .strip_prefix(PREFIX)
1244            .and_then(|value| value.strip_suffix(SUFFIX))
1245            .ok_or(NativeMountError::InvalidDestination)?;
1246        if digest.len() != 64
1247            || !digest
1248                .bytes()
1249                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1250        {
1251            return Err(NativeMountError::InvalidDestination);
1252        }
1253        let path = entry.path();
1254        let lock = match acquire_existing_mount_lock(&path) {
1255            Ok(lock) => lock,
1256            Err(NativeMountError::DestinationBusy) => continue,
1257            Err(error) => return Err(error),
1258        };
1259        drop(lock);
1260        match std::fs::remove_file(&path) {
1261            Ok(()) => {
1262                reclaimed = reclaimed.checked_add(1).ok_or_else(|| {
1263                    NativeMountError::Driver("native mount fence count overflow".to_owned())
1264                })?;
1265            }
1266            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1267            Err(error) => return Err(NativeMountError::Driver(error.to_string())),
1268        }
1269    }
1270    drop(registry);
1271    Ok(reclaimed)
1272}
1273
1274/// Recovers one implementation-owned disposable native destination after its
1275/// provider process dies.
1276///
1277/// The exact destination fence is acquired before any detach attempt, so a
1278/// surviving owner is never unmounted. Recovery then performs the platform's
1279/// bounded detach, removes only the now-exposed destination directory, and
1280/// releases the persistent fence under the parent registry lock. A nonempty
1281/// ordinary directory is retained and rejected rather than recursively erased.
1282///
1283/// # Errors
1284///
1285/// Returns [`NativeMountError::DestinationBusy`] for a live owner and a typed
1286/// driver or destination failure if detach, bounded verification, directory
1287/// removal, or fence release cannot complete exactly.
1288pub fn recover_native_mount_destination(destination: &Path) -> Result<(), NativeMountError> {
1289    let mut guard = MountDestinationGuard::acquire_for_recovery(destination)?;
1290    recover_platform_destination(destination).map_err(|error| {
1291        NativeMountError::Driver(format!("native platform recovery failed: {error}"))
1292    })?;
1293    match std::fs::remove_dir(destination) {
1294        Ok(()) => {}
1295        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1296        Err(error) => {
1297            return Err(NativeMountError::Driver(format!(
1298                "native recovered destination removal failed for {}: {error}",
1299                destination.display()
1300            )));
1301        }
1302    }
1303    guard.release()
1304}
1305
1306/// Reclaims a Windows provider's dead mount path without deleting authored residue.
1307///
1308/// The caller must own this implementation-managed destination. A stale `ProjFS`
1309/// cache, or an ordinary nonempty directory left by fail-open tools, is moved
1310/// to a unique sibling before the destination is reused. The returned path is
1311/// the preserved residue; `None` means the destination was already empty or
1312/// absent. A live provider remains fenced and cannot be recovered.
1313///
1314/// # Errors
1315///
1316/// Rejects a live owner, an unexpected reparse point, or a failed quarantine.
1317#[cfg(windows)]
1318pub fn recover_native_mount_destination_preserving_residue(
1319    destination: &Path,
1320) -> Result<Option<PathBuf>, NativeMountError> {
1321    let name = destination
1322        .file_name()
1323        .ok_or(NativeMountError::InvalidDestination)?;
1324    let parent = destination
1325        .parent()
1326        .ok_or(NativeMountError::InvalidDestination)?
1327        .canonicalize()
1328        .map_err(|_| NativeMountError::InvalidDestination)?;
1329    let destination = parent.join(name);
1330    let mut guard = MountDestinationGuard::acquire_for_recovery(&destination)?;
1331    let preserved = projfs::quarantine_crashed_destination(&destination)?;
1332    guard.release()?;
1333    Ok(preserved)
1334}
1335
1336/// Detaches and unfences one crash-left native destination while retaining its
1337/// underlying directory and authored host residue for an explicit caller
1338/// decision.
1339///
1340/// This is the host restart boundary: a live owner is rejected before any
1341/// detach, while a dead owner's kernel projection and persistent path fence are
1342/// removed. Call [`recover_native_mount_destination`] when the destination
1343/// itself is an implementation-owned disposable root.
1344///
1345/// # Errors
1346///
1347/// Returns a typed busy, detach, verification, or fence-release failure.
1348pub fn detach_native_mount_destination_after_crash(
1349    destination: &Path,
1350) -> Result<(), NativeMountError> {
1351    let mut guard = MountDestinationGuard::acquire_for_recovery(destination)?;
1352    detach_platform_destination_after_crash(destination)?;
1353    guard.release()
1354}
1355
1356#[cfg(any(target_os = "linux", target_os = "macos"))]
1357fn detach_platform_destination_after_crash(destination: &Path) -> Result<(), NativeMountError> {
1358    recover_platform_destination(destination)
1359}
1360
1361#[cfg(target_os = "windows")]
1362#[allow(clippy::unnecessary_wraps)]
1363fn detach_platform_destination_after_crash(_destination: &Path) -> Result<(), NativeMountError> {
1364    // ProjFS binds the virtualization context to its provider process. Process
1365    // termination detaches that context, while the authenticated local cache
1366    // and any authored residue remain at the virtualization root.
1367    Ok(())
1368}
1369
1370#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1371fn detach_platform_destination_after_crash(_destination: &Path) -> Result<(), NativeMountError> {
1372    Err(NativeMountError::UnsupportedTarget)
1373}
1374
1375fn start_owned_session<D>(
1376    destination: &Path,
1377    start: impl FnOnce() -> Result<D, DriverStartFailure>,
1378) -> Result<(D, MountDestinationGuard), NativeMountError> {
1379    let destination_guard = admit_destination(destination)?;
1380    finish_started_session(destination_guard, start())
1381}
1382
1383fn finish_started_session<D>(
1384    destination_guard: MountDestinationGuard,
1385    result: Result<D, DriverStartFailure>,
1386) -> Result<(D, MountDestinationGuard), NativeMountError> {
1387    match result {
1388        Ok(driver) => Ok((driver, destination_guard)),
1389        Err(failure) => {
1390            if failure.preserve_destination_fence {
1391                // Startup left an unknown kernel namespace behind. Keep the
1392                // process-owned lock live until process exit rather than
1393                // publishing the destination as reusable.
1394                std::mem::forget(destination_guard);
1395            }
1396            Err(failure.error)
1397        }
1398    }
1399}
1400
1401/// Same as [`start_owned_session`], but admits an existing non-empty
1402/// destination (see [`admit_destination_over_existing`]).
1403#[cfg(not(windows))]
1404fn start_owned_session_over_existing<D>(
1405    destination: &Path,
1406    start: impl FnOnce() -> Result<D, DriverStartFailure>,
1407) -> Result<(D, MountDestinationGuard), NativeMountError> {
1408    let destination_guard = admit_destination_over_existing(destination)?;
1409    finish_started_session(destination_guard, start())
1410}
1411
1412fn admit_destination(destination: &Path) -> Result<MountDestinationGuard, NativeMountError> {
1413    let guard = MountDestinationGuard::acquire(destination)?;
1414    validate_destination(destination)?;
1415    Ok(guard)
1416}
1417
1418/// Same process-fence as [`admit_destination`], but admits an existing
1419/// non-empty directory instead of requiring emptiness.
1420#[cfg(not(windows))]
1421fn admit_destination_over_existing(
1422    destination: &Path,
1423) -> Result<MountDestinationGuard, NativeMountError> {
1424    let guard = MountDestinationGuard::acquire(destination)?;
1425    validate_directory(destination)?;
1426    Ok(guard)
1427}
1428
1429#[derive(Debug)]
1430struct MountDestinationGuard {
1431    file: Option<File>,
1432    lock_path: PathBuf,
1433    registry_path: PathBuf,
1434}
1435
1436impl MountDestinationGuard {
1437    fn acquire(destination: &Path) -> Result<Self, NativeMountError> {
1438        let canonical = destination
1439            .canonicalize()
1440            .map_err(|_| NativeMountError::InvalidDestination)?;
1441        Self::acquire_canonical(&canonical)
1442    }
1443
1444    fn acquire_for_recovery(destination: &Path) -> Result<Self, NativeMountError> {
1445        let name = destination
1446            .file_name()
1447            .ok_or(NativeMountError::InvalidDestination)?;
1448        let parent = destination
1449            .parent()
1450            .ok_or(NativeMountError::InvalidDestination)?
1451            .canonicalize()
1452            .map_err(|_| NativeMountError::InvalidDestination)?;
1453        Self::acquire_canonical(&parent.join(name))
1454    }
1455
1456    fn acquire_canonical(canonical: &Path) -> Result<Self, NativeMountError> {
1457        let (lock_path, registry_path) = Self::paths_for_canonical(canonical)?;
1458        let registry = acquire_mount_lock(&registry_path, true)?;
1459        let file = acquire_mount_lock(&lock_path, false)?;
1460        drop(registry);
1461        Ok(Self {
1462            file: Some(file),
1463            lock_path,
1464            registry_path,
1465        })
1466    }
1467
1468    fn paths_for_canonical(canonical: &Path) -> Result<(PathBuf, PathBuf), NativeMountError> {
1469        let parent = canonical
1470            .parent()
1471            .ok_or(NativeMountError::InvalidDestination)?;
1472        let mut identity = blake3::Hasher::new();
1473        identity.update(b"acyclic-fs/native-mount-destination/v1\0");
1474        update_path_identity(&mut identity, canonical);
1475        let lock_path = parent.join(format!(
1476            ".acyclic-fs-mount-{}.lock",
1477            identity.finalize().to_hex()
1478        ));
1479        let registry_path = parent.join(".acyclic-fs-mount-registry.lock");
1480        Ok((lock_path, registry_path))
1481    }
1482
1483    fn reclaim_canonical(canonical: &Path) -> Result<(), NativeMountError> {
1484        let (lock_path, registry_path) = Self::paths_for_canonical(canonical)?;
1485        let registry = acquire_mount_lock(&registry_path, true)?;
1486        if !lock_path
1487            .try_exists()
1488            .map_err(|error| NativeMountError::Driver(error.to_string()))?
1489        {
1490            return Ok(());
1491        }
1492        let file = acquire_existing_mount_lock(&lock_path)?;
1493        drop(file);
1494        match std::fs::remove_file(&lock_path) {
1495            Ok(()) => {}
1496            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1497            Err(error) => return Err(NativeMountError::Driver(error.to_string())),
1498        }
1499        drop(registry);
1500        Ok(())
1501    }
1502
1503    fn release(&mut self) -> Result<(), NativeMountError> {
1504        let registry = acquire_mount_lock(&self.registry_path, true)?;
1505        release_owned_fence(&mut self.file, || {
1506            match std::fs::remove_file(&self.lock_path) {
1507                Ok(()) => {}
1508                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1509                Err(error) => return Err(NativeMountError::Driver(error.to_string())),
1510            }
1511            Ok(())
1512        })?;
1513        drop(registry);
1514        Ok(())
1515    }
1516}
1517
1518fn release_owned_fence<T, E>(
1519    owner: &mut Option<T>,
1520    release: impl FnOnce() -> Result<(), E>,
1521) -> Result<(), E> {
1522    release()?;
1523    owner.take();
1524    Ok(())
1525}
1526
1527#[cfg(target_os = "linux")]
1528fn recover_platform_destination(destination: &Path) -> Result<(), NativeMountError> {
1529    use std::process::{Command, Stdio};
1530
1531    if !linux_destination_is_mounted(destination)? {
1532        return Ok(());
1533    }
1534    let helper = [
1535        "/usr/bin/fusermount3",
1536        "/bin/fusermount3",
1537        "/usr/bin/fusermount",
1538        "/bin/fusermount",
1539    ]
1540    .into_iter()
1541    .find(|candidate| Path::new(candidate).is_file())
1542    .ok_or_else(|| {
1543        NativeMountError::CapabilityUnavailable(
1544            "no canonical fusermount helper is available for crash recovery".to_owned(),
1545        )
1546    })?;
1547    let mut child = Command::new(helper)
1548        .args(["-u", "-q", "-z", "--"])
1549        .arg(destination)
1550        .stdin(Stdio::null())
1551        .stdout(Stdio::null())
1552        .stderr(Stdio::null())
1553        .spawn()
1554        .map_err(|error| NativeMountError::Driver(error.to_string()))?;
1555    let deadline = Instant::now() + Duration::from_secs(30);
1556    loop {
1557        if child
1558            .try_wait()
1559            .map_err(|error| NativeMountError::Driver(error.to_string()))?
1560            .is_some()
1561        {
1562            break;
1563        }
1564        if Instant::now() >= deadline {
1565            let _ = child.kill();
1566            let _ = child.wait();
1567            return Err(NativeMountError::Driver(
1568                "fusermount crash recovery exceeded 30 seconds".to_owned(),
1569            ));
1570        }
1571        std::thread::sleep(Duration::from_millis(10));
1572    }
1573    if linux_destination_is_mounted(destination)? {
1574        return Err(NativeMountError::Driver(
1575            "FUSE destination remained mounted after crash recovery".to_owned(),
1576        ));
1577    }
1578    Ok(())
1579}
1580
1581#[cfg(target_os = "linux")]
1582fn linux_destination_is_mounted(destination: &Path) -> Result<bool, NativeMountError> {
1583    use std::os::unix::fs::MetadataExt;
1584
1585    let parent = destination
1586        .parent()
1587        .ok_or(NativeMountError::InvalidDestination)?
1588        .metadata()
1589        .map_err(|error| NativeMountError::Driver(error.to_string()))?;
1590    match destination.metadata() {
1591        Ok(metadata) => Ok(metadata.dev() != parent.dev()),
1592        Err(error) if error.raw_os_error() == Some(libc::ENOTCONN) => Ok(true),
1593        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
1594        Err(error) => Err(NativeMountError::Driver(error.to_string())),
1595    }
1596}
1597
1598#[cfg(target_os = "macos")]
1599fn recover_platform_destination(destination: &Path) -> Result<(), NativeMountError> {
1600    darwin_mount::recover_destination(destination)
1601}
1602
1603#[cfg(target_os = "windows")]
1604fn recover_platform_destination(destination: &Path) -> Result<(), NativeMountError> {
1605    projfs::recover_cache_only_destination(destination)
1606}
1607
1608#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1609fn recover_platform_destination(_destination: &Path) -> Result<(), NativeMountError> {
1610    Err(NativeMountError::UnsupportedTarget)
1611}
1612
1613impl Drop for MountDestinationGuard {
1614    fn drop(&mut self) {
1615        let _ = self.release();
1616    }
1617}
1618
1619fn acquire_mount_lock(path: &Path, blocking: bool) -> Result<File, NativeMountError> {
1620    let file = OpenOptions::new()
1621        .create(true)
1622        .truncate(false)
1623        .read(true)
1624        .write(true)
1625        .open(path)
1626        .map_err(|error| NativeMountError::Driver(error.to_string()))?;
1627    let lock_metadata = std::fs::symlink_metadata(path)
1628        .map_err(|error| NativeMountError::Driver(error.to_string()))?;
1629    if lock_metadata.file_type().is_symlink() || !file.metadata().is_ok_and(|value| value.is_file())
1630    {
1631        return Err(NativeMountError::InvalidDestination);
1632    }
1633    if blocking {
1634        fs2::FileExt::lock_exclusive(&file)
1635            .map_err(|error| NativeMountError::Driver(error.to_string()))?;
1636    } else {
1637        fs2::FileExt::try_lock_exclusive(&file).map_err(|error| {
1638            if lock_is_contended(&error) {
1639                NativeMountError::DestinationBusy
1640            } else {
1641                NativeMountError::Driver(error.to_string())
1642            }
1643        })?;
1644    }
1645    Ok(file)
1646}
1647
1648fn acquire_existing_mount_lock(path: &Path) -> Result<File, NativeMountError> {
1649    let file = OpenOptions::new()
1650        .read(true)
1651        .write(true)
1652        .open(path)
1653        .map_err(|error| NativeMountError::Driver(error.to_string()))?;
1654    let lock_metadata = std::fs::symlink_metadata(path)
1655        .map_err(|error| NativeMountError::Driver(error.to_string()))?;
1656    if lock_metadata.file_type().is_symlink() || !file.metadata().is_ok_and(|value| value.is_file())
1657    {
1658        return Err(NativeMountError::InvalidDestination);
1659    }
1660    fs2::FileExt::try_lock_exclusive(&file).map_err(|error| {
1661        if lock_is_contended(&error) {
1662            NativeMountError::DestinationBusy
1663        } else {
1664            NativeMountError::Driver(error.to_string())
1665        }
1666    })?;
1667    Ok(file)
1668}
1669
1670fn lock_is_contended(error: &std::io::Error) -> bool {
1671    if error.kind() == std::io::ErrorKind::WouldBlock {
1672        return true;
1673    }
1674    #[cfg(windows)]
1675    {
1676        // LockFileEx reports ERROR_LOCK_VIOLATION rather than WouldBlock.
1677        error.raw_os_error() == Some(33)
1678    }
1679    #[cfg(not(windows))]
1680    {
1681        false
1682    }
1683}
1684
1685#[cfg(unix)]
1686fn update_path_identity(hasher: &mut blake3::Hasher, path: &Path) {
1687    use std::os::unix::ffi::OsStrExt;
1688    hasher.update(path.as_os_str().as_bytes());
1689}
1690
1691#[cfg(windows)]
1692fn update_path_identity(hasher: &mut blake3::Hasher, path: &Path) {
1693    use std::os::windows::ffi::OsStrExt;
1694    for unit in path.as_os_str().encode_wide() {
1695        hasher.update(&unit.to_le_bytes());
1696    }
1697}
1698
1699#[cfg(not(any(unix, windows)))]
1700fn update_path_identity(hasher: &mut blake3::Hasher, path: &Path) {
1701    hasher.update(path.to_string_lossy().as_bytes());
1702}
1703
1704fn validate_destination(destination: &Path) -> Result<(), NativeMountError> {
1705    validate_directory(destination)?;
1706    let mut entries =
1707        std::fs::read_dir(destination).map_err(|_| NativeMountError::InvalidDestination)?;
1708    if entries.next().is_some() {
1709        return Err(NativeMountError::InvalidDestination);
1710    }
1711    Ok(())
1712}
1713
1714/// The directory half of [`validate_destination`], without the
1715/// empty-directory requirement — shared with
1716/// [`mount_native_over_existing`]'s admission path.
1717fn validate_directory(destination: &Path) -> Result<(), NativeMountError> {
1718    let metadata =
1719        std::fs::symlink_metadata(destination).map_err(|_| NativeMountError::InvalidDestination)?;
1720    if !metadata.is_dir() || metadata.file_type().is_symlink() {
1721        return Err(NativeMountError::InvalidDestination);
1722    }
1723    Ok(())
1724}
1725
1726#[cfg(target_os = "linux")]
1727fn start_driver(
1728    request: &NativeMountRequest,
1729    source: Arc<dyn MountFilesystem>,
1730) -> Result<DriverSession, DriverStartFailure> {
1731    fuse::FuseSession::start(request, source)
1732        .map(DriverSession::Fuse)
1733        .map_err(Into::into)
1734}
1735
1736#[cfg(target_os = "macos")]
1737fn start_driver(
1738    request: &NativeMountRequest,
1739    source: Arc<dyn MountFilesystem>,
1740) -> Result<DriverSession, DriverStartFailure> {
1741    darwin_mount::DarwinMountSession::start(request, source).map(DriverSession::DarwinMount)
1742}
1743
1744#[cfg(target_os = "windows")]
1745fn start_driver(
1746    request: &NativeMountRequest,
1747    source: Arc<dyn MountFilesystem>,
1748) -> Result<DriverSession, DriverStartFailure> {
1749    projfs::ProjFsSession::start(request, source).map(DriverSession::ProjFs)
1750}
1751
1752#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1753fn start_driver(
1754    _request: &NativeMountRequest,
1755    _source: Arc<dyn MountFilesystem>,
1756) -> Result<DriverSession, DriverStartFailure> {
1757    Err(NativeMountError::UnsupportedTarget.into())
1758}
1759
1760fn stop_driver(driver: &mut DriverSession) -> Result<(), NativeMountError> {
1761    match driver {
1762        #[cfg(target_os = "linux")]
1763        DriverSession::Fuse(session) => session.stop(),
1764        #[cfg(target_os = "macos")]
1765        DriverSession::DarwinMount(session) => session.stop(),
1766        #[cfg(target_os = "windows")]
1767        DriverSession::ProjFs(session) => session.stop(),
1768        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1769        DriverSession::Unsupported => Err(NativeMountError::UnsupportedTarget),
1770    }
1771}
1772
1773#[cfg(target_os = "linux")]
1774mod platform {
1775    use super::{NativeMountCapabilities, NativeMountKind, NativeMountSessionIsolation};
1776
1777    pub(super) fn probe() -> NativeMountCapabilities {
1778        let available = std::fs::OpenOptions::new()
1779            .read(true)
1780            .write(true)
1781            .open("/dev/fuse")
1782            .is_ok();
1783        NativeMountCapabilities {
1784            kind: Some(NativeMountKind::LinuxFuse),
1785            available,
1786            writable: available,
1787            provider_process_io_observable: available,
1788            session_isolation: NativeMountSessionIsolation::SharedProcess,
1789            unavailable_reason: (!available).then(|| "/dev/fuse is not usable".to_owned()),
1790        }
1791    }
1792}
1793
1794#[cfg(target_os = "macos")]
1795mod platform {
1796    use super::{NativeMountCapabilities, NativeMountKind, NativeMountSessionIsolation};
1797
1798    pub(super) fn probe() -> NativeMountCapabilities {
1799        let available = std::path::Path::new("/sbin/mount_nfs").is_file()
1800            && std::path::Path::new("/sbin/umount").is_file();
1801        NativeMountCapabilities {
1802            kind: Some(NativeMountKind::MacOsNfs),
1803            available,
1804            writable: available,
1805            provider_process_io_observable: available,
1806            session_isolation: NativeMountSessionIsolation::SharedProcess,
1807            unavailable_reason: (!available)
1808                .then(|| "the built-in macOS NFS mount utilities are unavailable".to_owned()),
1809        }
1810    }
1811}
1812
1813#[cfg(target_os = "windows")]
1814mod platform {
1815    use super::{NativeMountCapabilities, NativeMountKind, NativeMountSessionIsolation};
1816
1817    pub(super) fn probe() -> NativeMountCapabilities {
1818        let system_root = std::env::var_os("SystemRoot").unwrap_or_else(|| "C:\\Windows".into());
1819        let available = std::path::Path::new(&system_root)
1820            .join("System32")
1821            .join("ProjectedFSLib.dll")
1822            .is_file();
1823        NativeMountCapabilities {
1824            kind: Some(NativeMountKind::WindowsProjFs),
1825            available,
1826            writable: available,
1827            provider_process_io_observable: false,
1828            session_isolation: NativeMountSessionIsolation::SharedProcess,
1829            unavailable_reason: (!available)
1830                .then(|| "ProjectedFSLib.dll is unavailable".to_owned()),
1831        }
1832    }
1833}
1834
1835#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1836mod platform {
1837    use super::{NativeMountCapabilities, NativeMountSessionIsolation};
1838
1839    pub(super) fn probe() -> NativeMountCapabilities {
1840        NativeMountCapabilities {
1841            kind: None,
1842            available: false,
1843            writable: false,
1844            provider_process_io_observable: false,
1845            session_isolation: NativeMountSessionIsolation::ProcessIsolated,
1846            unavailable_reason: Some("unsupported target".to_owned()),
1847        }
1848    }
1849}
1850
1851#[cfg(test)]
1852mod tests {
1853    use super::*;
1854    use crate::kernel::{LogicalName, NameEncoding, NamespacePath};
1855    use crate::model::{
1856        AccessMode, CaseSensitivity, CheckoutMode, ConcurrencyMode, ConsistencyMode,
1857        FilesystemProfile, GenerationSelector, Lifecycle, MutationMode, UnicodePolicy,
1858        VolumeConfig, VolumeLimits,
1859    };
1860    use crate::path::PortablePath;
1861    use crate::{
1862        ByteRange, CancellationToken, Fs, VolumeId, WatchBatch, WatchChange, WatchEpoch,
1863        WatchSequence, WorkBudget,
1864    };
1865    use bytes::Bytes;
1866    use std::time::{Duration, Instant};
1867
1868    #[cfg(windows)]
1869    #[test]
1870    fn projfs_cannot_shadow_an_existing_directory() -> Result<(), Box<dyn std::error::Error>> {
1871        let root = tempfile::tempdir()?;
1872        std::fs::write(root.path().join("existing.txt"), b"real")?;
1873        let request = NativeMountRequest {
1874            mount_id: MountId::new(),
1875            volume_id: VolumeId::new(),
1876            destination: root.path().to_path_buf(),
1877            writable: true,
1878        };
1879        let source = Arc::new(RoutedMountSource::new());
1880        assert!(matches!(
1881            mount_native_over_existing(request, source),
1882            Err(NativeMountError::WritableUnavailable(_))
1883        ));
1884        assert_eq!(std::fs::read(root.path().join("existing.txt"))?, b"real");
1885        Ok(())
1886    }
1887
1888    fn portable_path(
1889        limits: VolumeLimits,
1890        components: &[&[u8]],
1891    ) -> Result<NamespacePath, Box<dyn std::error::Error>> {
1892        Ok(NamespacePath::new(
1893            components
1894                .iter()
1895                .map(|name| {
1896                    LogicalName::new(
1897                        NameEncoding::Utf8,
1898                        name.to_vec(),
1899                        limits.maximum_component_bytes,
1900                    )
1901                })
1902                .collect::<Result<Vec<_>, _>>()?,
1903            limits,
1904        )?)
1905    }
1906
1907    #[test]
1908    fn probe_names_exactly_one_target_driver() {
1909        let probe = probe_native_mount();
1910        assert!(probe.kind.is_some());
1911        assert_eq!(probe.available, probe.unavailable_reason.is_none());
1912    }
1913
1914    #[test]
1915    fn destination_admission_rejects_nonempty_directories() {
1916        let root =
1917            std::env::temp_dir().join(format!("acyclic-fs-mount-admission-{}", std::process::id()));
1918        let _ = std::fs::remove_dir_all(&root);
1919        assert!(std::fs::create_dir_all(&root).is_ok());
1920        assert!(std::fs::write(root.join("occupied"), b"x").is_ok());
1921        assert!(matches!(
1922            validate_destination(&root),
1923            Err(NativeMountError::InvalidDestination)
1924        ));
1925        let _ = std::fs::remove_dir_all(root);
1926    }
1927
1928    #[test]
1929    fn validate_directory_admits_nonempty_directories_but_still_rejects_non_directories()
1930    -> Result<(), Box<dyn std::error::Error>> {
1931        let root = std::env::temp_dir().join(format!(
1932            "acyclic-fs-mount-over-existing-{}",
1933            std::process::id()
1934        ));
1935        let _ = std::fs::remove_dir_all(&root);
1936        std::fs::create_dir_all(&root)?;
1937        std::fs::write(root.join("occupied"), b"x")?;
1938        assert!(validate_directory(&root).is_ok());
1939
1940        let file = root.join("occupied");
1941        assert!(matches!(
1942            validate_directory(&file),
1943            Err(NativeMountError::InvalidDestination)
1944        ));
1945
1946        let missing = root.join("does-not-exist");
1947        assert!(matches!(
1948            validate_directory(&missing),
1949            Err(NativeMountError::InvalidDestination)
1950        ));
1951
1952        let _ = std::fs::remove_dir_all(&root);
1953        Ok(())
1954    }
1955
1956    #[test]
1957    fn crash_recovery_never_removes_live_or_contaminated_destinations()
1958    -> Result<(), Box<dyn std::error::Error>> {
1959        let parent = tempfile::tempdir()?;
1960        let destination = parent.path().join("native-destination");
1961        std::fs::create_dir(&destination)?;
1962
1963        let live_guard = MountDestinationGuard::acquire(&destination)?;
1964        assert!(matches!(
1965            recover_native_mount_destination(&destination),
1966            Err(NativeMountError::DestinationBusy)
1967        ));
1968        assert!(destination.is_dir());
1969        drop(live_guard);
1970
1971        std::fs::write(destination.join("unowned-content"), b"retain")?;
1972        assert!(matches!(
1973            recover_native_mount_destination(&destination),
1974            Err(NativeMountError::Driver(_))
1975        ));
1976        assert_eq!(
1977            std::fs::read(destination.join("unowned-content"))?,
1978            b"retain"
1979        );
1980        std::fs::remove_file(destination.join("unowned-content"))?;
1981
1982        recover_native_mount_destination(&destination)?;
1983        assert!(!destination.exists());
1984        let residue = std::fs::read_dir(parent.path())?
1985            .filter_map(Result::ok)
1986            .map(|entry| entry.file_name())
1987            .filter(|name| name != ".acyclic-fs-mount-registry.lock")
1988            .collect::<Vec<_>>();
1989        assert!(residue.is_empty());
1990        Ok(())
1991    }
1992
1993    #[cfg(windows)]
1994    #[test]
1995    fn preserving_recovery_fences_live_owner_and_retains_ordinary_residue()
1996    -> Result<(), Box<dyn std::error::Error>> {
1997        let parent = tempfile::tempdir()?;
1998        let destination = parent.path().join("mount");
1999        std::fs::create_dir(&destination)?;
2000        let live_guard = MountDestinationGuard::acquire(&destination)?;
2001        assert!(matches!(
2002            recover_native_mount_destination_preserving_residue(&destination),
2003            Err(NativeMountError::DestinationBusy)
2004        ));
2005        drop(live_guard);
2006
2007        std::fs::write(destination.join("authored.txt"), b"retain")?;
2008        let preserved = recover_native_mount_destination_preserving_residue(&destination)?
2009            .ok_or("nonempty destination was not preserved")?;
2010        assert!(!destination.exists());
2011        assert_eq!(std::fs::read(preserved.join("authored.txt"))?, b"retain");
2012        assert!(recover_native_mount_destination_preserving_residue(&destination)?.is_none());
2013        Ok(())
2014    }
2015
2016    #[test]
2017    fn failed_destination_admission_releases_ownership() -> Result<(), Box<dyn std::error::Error>> {
2018        let temporary = tempfile::tempdir()?;
2019        let destination = temporary.path().join("mount");
2020        std::fs::create_dir(&destination)?;
2021        std::fs::write(destination.join("occupied"), b"x")?;
2022        assert!(matches!(
2023            admit_destination(&destination),
2024            Err(NativeMountError::InvalidDestination)
2025        ));
2026        std::fs::remove_file(destination.join("occupied"))?;
2027        let _recovered = admit_destination(&destination)?;
2028        Ok(())
2029    }
2030
2031    #[test]
2032    fn failed_driver_start_releases_destination_ownership() -> Result<(), Box<dyn std::error::Error>>
2033    {
2034        let temporary = tempfile::tempdir()?;
2035        let destination = temporary.path().join("mount");
2036        std::fs::create_dir(&destination)?;
2037
2038        let failed = start_owned_session::<()>(&destination, || {
2039            Err(NativeMountError::Driver("injected startup failure".to_owned()).into())
2040        });
2041        assert!(matches!(failed, Err(NativeMountError::Driver(_))));
2042        let recovered_after_error = MountDestinationGuard::acquire(&destination)?;
2043        drop(recovered_after_error);
2044
2045        Ok(())
2046    }
2047
2048    #[test]
2049    fn destination_ownership_is_process_external_and_recoverable()
2050    -> Result<(), Box<dyn std::error::Error>> {
2051        let temporary = tempfile::tempdir()?;
2052        let destination = temporary.path().join("mount");
2053        std::fs::create_dir(&destination)?;
2054        let first = MountDestinationGuard::acquire(&destination)?;
2055        let second = MountDestinationGuard::acquire(&destination);
2056        assert!(
2057            matches!(second, Err(NativeMountError::DestinationBusy)),
2058            "unexpected second-owner result: {second:?}"
2059        );
2060        let independent = temporary.path().join("independent");
2061        std::fs::create_dir(&independent)?;
2062        let _independent = MountDestinationGuard::acquire(&independent)?;
2063        drop(first);
2064        let _recovered = MountDestinationGuard::acquire(&destination)?;
2065        Ok(())
2066    }
2067
2068    #[test]
2069    fn orderly_destination_release_removes_per_mount_fence()
2070    -> Result<(), Box<dyn std::error::Error>> {
2071        let temporary = tempfile::tempdir()?;
2072        let destination = temporary.path().join("mount");
2073        std::fs::create_dir(&destination)?;
2074        let guard = MountDestinationGuard::acquire(&destination)?;
2075        drop(guard);
2076
2077        let mut residue = std::fs::read_dir(temporary.path())?
2078            .map(|entry| entry.map(|value| value.file_name()))
2079            .collect::<Result<Vec<_>, _>>()?;
2080        residue.sort();
2081        assert_eq!(
2082            residue,
2083            vec![
2084                std::ffi::OsString::from(".acyclic-fs-mount-registry.lock"),
2085                std::ffi::OsString::from("mount"),
2086            ]
2087        );
2088        Ok(())
2089    }
2090
2091    #[test]
2092    fn repeated_admission_teardown_has_constant_persistent_resource_set()
2093    -> Result<(), Box<dyn std::error::Error>> {
2094        let temporary = tempfile::tempdir()?;
2095        let destinations = [
2096            temporary.path().join("mount-a"),
2097            temporary.path().join("mount-b"),
2098        ];
2099        for destination in &destinations {
2100            std::fs::create_dir(destination)?;
2101        }
2102        for ordinal in 0..1_024 {
2103            let guard = MountDestinationGuard::acquire(&destinations[ordinal % 2])?;
2104            drop(guard);
2105        }
2106        let mut residue = std::fs::read_dir(temporary.path())?
2107            .map(|entry| entry.map(|value| value.file_name()))
2108            .collect::<Result<Vec<_>, _>>()?;
2109        residue.sort();
2110        assert_eq!(
2111            residue,
2112            vec![
2113                std::ffi::OsString::from(".acyclic-fs-mount-registry.lock"),
2114                std::ffi::OsString::from("mount-a"),
2115                std::ffi::OsString::from("mount-b"),
2116            ]
2117        );
2118        for destination in destinations {
2119            std::fs::remove_dir(destination)?;
2120        }
2121        Ok(())
2122    }
2123
2124    #[test]
2125    fn bounded_parent_sweep_reclaims_stale_fences_and_preserves_live_owners()
2126    -> Result<(), Box<dyn std::error::Error>> {
2127        let temporary = tempfile::tempdir()?;
2128        let live_destination = temporary.path().join("live");
2129        let stale_destination = temporary.path().join("stale");
2130        std::fs::create_dir(&live_destination)?;
2131        std::fs::create_dir(&stale_destination)?;
2132        let live = MountDestinationGuard::acquire(&live_destination)?;
2133        let mut stale = MountDestinationGuard::acquire(&stale_destination)?;
2134        let live_path = live.lock_path.clone();
2135        let stale_path = stale.lock_path.clone();
2136        stale.file.take();
2137        std::mem::forget(stale);
2138
2139        assert_eq!(
2140            reclaim_stale_native_mount_destination_fences(temporary.path())?,
2141            1
2142        );
2143        assert!(live_path.is_file());
2144        assert!(!stale_path.exists());
2145        drop(live);
2146        assert_eq!(
2147            reclaim_stale_native_mount_destination_fences(temporary.path())?,
2148            0
2149        );
2150        Ok(())
2151    }
2152
2153    #[test]
2154    fn parent_sweep_rejects_malformed_reserved_fence_names()
2155    -> Result<(), Box<dyn std::error::Error>> {
2156        let temporary = tempfile::tempdir()?;
2157        std::fs::write(temporary.path().join(".acyclic-fs-mount-invalid.lock"), b"")?;
2158        assert!(matches!(
2159            reclaim_stale_native_mount_destination_fences(temporary.path()),
2160            Err(NativeMountError::InvalidDestination)
2161        ));
2162        Ok(())
2163    }
2164
2165    #[test]
2166    fn destination_reclaim_child_waits_for_external_release()
2167    -> Result<(), Box<dyn std::error::Error>> {
2168        let Some(destination) = std::env::var_os("ACYCLIC_FS_RECLAIM_CHILD_DESTINATION") else {
2169            return Ok(());
2170        };
2171        let ready = std::env::var_os("ACYCLIC_FS_RECLAIM_CHILD_READY")
2172            .ok_or("reclaim child readiness path is absent")?;
2173        let release = std::env::var_os("ACYCLIC_FS_RECLAIM_CHILD_RELEASE")
2174            .ok_or("reclaim child release path is absent")?;
2175        std::fs::write(ready, b"ready")?;
2176        let deadline = Instant::now() + Duration::from_secs(10);
2177        while !Path::new(&release).is_file() {
2178            if Instant::now() >= deadline {
2179                return Err("reclaim child release timed out".into());
2180            }
2181            std::thread::sleep(Duration::from_millis(10));
2182        }
2183        reclaim_native_mount_destination_fence(Path::new(&destination))?;
2184        Ok(())
2185    }
2186
2187    #[test]
2188    fn independent_supervisors_concurrently_reclaim_one_crash_left_fence()
2189    -> Result<(), Box<dyn std::error::Error>> {
2190        struct KillChildren(Vec<std::process::Child>);
2191
2192        impl Drop for KillChildren {
2193            fn drop(&mut self) {
2194                for child in &mut self.0 {
2195                    let _ = child.kill();
2196                    let _ = child.wait();
2197                }
2198            }
2199        }
2200
2201        let temporary = tempfile::tempdir()?;
2202        let destination = temporary.path().join("mount");
2203        let release = temporary.path().join("release");
2204        std::fs::create_dir(&destination)?;
2205        let mut crashed = MountDestinationGuard::acquire(&destination)?;
2206        let crash_left_path = crashed.lock_path.clone();
2207        crashed.file.take();
2208        std::mem::forget(crashed);
2209
2210        let executable = std::env::current_exe()?;
2211        let mut children = KillChildren(Vec::new());
2212        let mut readiness = Vec::new();
2213        for ordinal in 0..2 {
2214            let ready = temporary.path().join(format!("ready-{ordinal}"));
2215            let child = std::process::Command::new(&executable)
2216                .args([
2217                    "--exact",
2218                    "native_mount::tests::destination_reclaim_child_waits_for_external_release",
2219                    "--nocapture",
2220                ])
2221                .env("ACYCLIC_FS_RECLAIM_CHILD_DESTINATION", &destination)
2222                .env("ACYCLIC_FS_RECLAIM_CHILD_READY", &ready)
2223                .env("ACYCLIC_FS_RECLAIM_CHILD_RELEASE", &release)
2224                .spawn()?;
2225            readiness.push(ready);
2226            children.0.push(child);
2227        }
2228        let deadline = Instant::now() + Duration::from_secs(10);
2229        while readiness.iter().any(|path| !path.is_file()) {
2230            for child in &mut children.0 {
2231                if let Some(status) = child.try_wait()? {
2232                    return Err(format!("reclaim child exited before release: {status}").into());
2233                }
2234            }
2235            if Instant::now() >= deadline {
2236                return Err("reclaim children did not become ready".into());
2237            }
2238            std::thread::sleep(Duration::from_millis(10));
2239        }
2240        std::fs::write(&release, b"release")?;
2241        for child in &mut children.0 {
2242            let status = child.wait()?;
2243            if !status.success() {
2244                return Err(format!("reclaim child failed: {status}").into());
2245            }
2246        }
2247        children.0.clear();
2248        assert!(!crash_left_path.exists());
2249        assert!(destination.is_dir());
2250        Ok(())
2251    }
2252
2253    #[test]
2254    fn destination_ownership_child_holds_lock_until_killed()
2255    -> Result<(), Box<dyn std::error::Error>> {
2256        let Some(destination) = std::env::var_os("ACYCLIC_FS_LOCK_CHILD_DESTINATION") else {
2257            return Ok(());
2258        };
2259        let ready = std::env::var_os("ACYCLIC_FS_LOCK_CHILD_READY")
2260            .ok_or("lock child readiness path is absent")?;
2261        let _guard = MountDestinationGuard::acquire(Path::new(&destination))?;
2262        std::fs::write(ready, b"ready")?;
2263        loop {
2264            std::thread::park();
2265        }
2266    }
2267
2268    #[test]
2269    fn destination_ownership_is_reclaimed_after_process_death()
2270    -> Result<(), Box<dyn std::error::Error>> {
2271        struct KillOnDrop(std::process::Child);
2272
2273        impl Drop for KillOnDrop {
2274            fn drop(&mut self) {
2275                let _ = self.0.kill();
2276                let _ = self.0.wait();
2277            }
2278        }
2279
2280        let temporary = tempfile::tempdir()?;
2281        let destination = temporary.path().join("mount");
2282        let ready = temporary.path().join("ready");
2283        std::fs::create_dir(&destination)?;
2284        let child = std::process::Command::new(std::env::current_exe()?)
2285            .args([
2286                "--exact",
2287                "native_mount::tests::destination_ownership_child_holds_lock_until_killed",
2288                "--nocapture",
2289            ])
2290            .env("ACYCLIC_FS_LOCK_CHILD_DESTINATION", &destination)
2291            .env("ACYCLIC_FS_LOCK_CHILD_READY", &ready)
2292            .spawn()?;
2293        let mut child = KillOnDrop(child);
2294        let deadline = Instant::now() + Duration::from_secs(10);
2295        while !ready.is_file() {
2296            if let Some(status) = child.0.try_wait()? {
2297                return Err(format!("lock child exited before readiness: {status}").into());
2298            }
2299            if Instant::now() >= deadline {
2300                return Err("lock child readiness timed out".into());
2301            }
2302            std::thread::sleep(Duration::from_millis(10));
2303        }
2304        assert!(matches!(
2305            MountDestinationGuard::acquire(&destination),
2306            Err(NativeMountError::DestinationBusy)
2307        ));
2308        assert!(matches!(
2309            reclaim_native_mount_destination_fence(&destination),
2310            Err(NativeMountError::DestinationBusy)
2311        ));
2312        child.0.kill()?;
2313        let status = child.0.wait()?;
2314        assert!(
2315            !status.success(),
2316            "killed lock child unexpectedly succeeded"
2317        );
2318        reclaim_native_mount_destination_fence(&destination)?;
2319        reclaim_native_mount_destination_fence(&destination)?;
2320        let _reclaimed = MountDestinationGuard::acquire(&destination)?;
2321        Ok(())
2322    }
2323
2324    #[test]
2325    fn failed_stop_retains_driver_and_destination_ownership_for_exact_retry() {
2326        let mut driver = Some(7_u8);
2327        let mut destination_guard = Some(11_u8);
2328        let failure = stop_owned_session(
2329            &mut driver,
2330            &mut destination_guard,
2331            |_| Err("busy"),
2332            |_| Ok(()),
2333        );
2334        assert_eq!(failure, Err("busy"));
2335        assert_eq!(driver, Some(7));
2336        assert_eq!(destination_guard, Some(11));
2337
2338        let stopped = stop_owned_session(
2339            &mut driver,
2340            &mut destination_guard,
2341            |_| Ok::<_, &str>(()),
2342            |_| Ok(()),
2343        );
2344        assert_eq!(stopped, Ok(true));
2345        assert_eq!(driver, None);
2346        assert_eq!(destination_guard, None);
2347        assert_eq!(
2348            stop_owned_session(
2349                &mut driver,
2350                &mut destination_guard,
2351                |_| Ok::<_, &str>(()),
2352                |_| Ok(()),
2353            ),
2354            Ok(false)
2355        );
2356    }
2357
2358    #[test]
2359    fn failed_final_stop_keeps_driver_and_destination_fence_until_process_exit() {
2360        struct DropProbe(Arc<std::sync::atomic::AtomicUsize>);
2361
2362        impl Drop for DropProbe {
2363            fn drop(&mut self) {
2364                self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2365            }
2366        }
2367
2368        let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2369        let mut driver = Some(DropProbe(Arc::clone(&drops)));
2370        let mut destination_guard = Some(DropProbe(Arc::clone(&drops)));
2371        drop_owned_session(
2372            &mut driver,
2373            &mut destination_guard,
2374            |_| Err("kernel detach failed"),
2375            |_| Ok(()),
2376        );
2377        assert!(driver.is_none());
2378        assert!(destination_guard.is_none());
2379        assert_eq!(drops.load(std::sync::atomic::Ordering::SeqCst), 0);
2380    }
2381
2382    #[test]
2383    fn failed_fence_unlink_retains_the_operating_system_lock() {
2384        let mut fence = Some(7_u8);
2385        let failure = release_owned_fence(&mut fence, || Err("unlink failed"));
2386        assert_eq!(failure, Err("unlink failed"));
2387        assert_eq!(fence, Some(7));
2388        assert_eq!(
2389            release_owned_fence(&mut fence, || Ok::<_, &str>(())),
2390            Ok(())
2391        );
2392        assert_eq!(fence, None);
2393    }
2394
2395    #[test]
2396    fn failed_fence_release_is_retried_without_restarting_the_driver() {
2397        let mut driver = Some(7_u8);
2398        let mut destination_guard = Some(11_u8);
2399        let mut first_release = true;
2400        let failure = stop_owned_session(
2401            &mut driver,
2402            &mut destination_guard,
2403            |_| Ok::<_, &str>(()),
2404            |_| {
2405                if first_release {
2406                    first_release = false;
2407                    Err("registry busy")
2408                } else {
2409                    Ok(())
2410                }
2411            },
2412        );
2413        assert_eq!(failure, Err("registry busy"));
2414        assert_eq!(driver, None);
2415        assert_eq!(destination_guard, Some(11));
2416
2417        let stopped = stop_owned_session(
2418            &mut driver,
2419            &mut destination_guard,
2420            |_| Err("driver must not restart"),
2421            |_| Ok(()),
2422        );
2423        assert_eq!(stopped, Ok(true));
2424        assert_eq!(destination_guard, None);
2425    }
2426
2427    #[tokio::test]
2428    #[allow(clippy::too_many_lines)]
2429    async fn capture_host_hard_links_preserves_shared_file_identity()
2430    -> Result<(), Box<dyn std::error::Error>> {
2431        let limits = VolumeLimits::default();
2432        let config = VolumeConfig {
2433            profile: FilesystemProfile::Portable,
2434            concurrency: ConcurrencyMode::Optimistic,
2435            lifecycle: Lifecycle::Ephemeral,
2436            case_sensitivity: CaseSensitivity::Sensitive,
2437            unicode: UnicodePolicy::Preserve,
2438            symbolic_links: true,
2439            hard_links: true,
2440            sparse_files: true,
2441            limits,
2442        };
2443        let cancellation = CancellationToken::new();
2444        let fs = Fs::memory();
2445        let volume = fs
2446            .create_volume_with_id(
2447                VolumeId::from_bytes([92; 16]),
2448                config,
2449                WorkBudget::UNBOUNDED,
2450                &cancellation,
2451            )
2452            .await?
2453            .value;
2454        let mut checkout = volume
2455            .checkout(
2456                GenerationSelector::Head,
2457                CheckoutMode {
2458                    access: AccessMode::ReadWrite,
2459                    consistency: ConsistencyMode::Pinned,
2460                    mutations: MutationMode::PrivateOverlay,
2461                },
2462                WorkBudget::UNBOUNDED,
2463                &cancellation,
2464            )
2465            .await?
2466            .value;
2467        let host = tempfile::tempdir()?;
2468        std::fs::write(host.path().join("a"), b"shared")?;
2469        std::fs::hard_link(host.path().join("a"), host.path().join("b"))?;
2470        let path = |name: &str| -> Result<NamespacePath, Box<dyn std::error::Error>> {
2471            Ok(NamespacePath::new(
2472                vec![LogicalName::new(
2473                    NameEncoding::Utf8,
2474                    name.as_bytes().to_vec(),
2475                    limits.maximum_component_bytes,
2476                )?],
2477                limits,
2478            )?)
2479        };
2480        let paths = [path("a")?, path("b")?];
2481        crate::native_capture::capture_paths_batched_with_baseline(
2482            &mut checkout,
2483            &paths,
2484            &CaptureOptions {
2485                expected_root_identity: capture_root_identity(host.path())?,
2486                source_root: host.path().to_path_buf(),
2487                maximum_paths: 2,
2488                maximum_extent_spans: 16,
2489            },
2490            1,
2491            WorkBudget::UNBOUNDED,
2492            &cancellation,
2493            None,
2494        )
2495        .await?;
2496        let first = checkout
2497            .lookup_no_follow(&paths[0], WorkBudget::UNBOUNDED, &cancellation)
2498            .await?
2499            .value
2500            .record
2501            .ok_or("first captured link missing")?;
2502        let second = checkout
2503            .lookup_no_follow(&paths[1], WorkBudget::UNBOUNDED, &cancellation)
2504            .await?
2505            .value
2506            .record
2507            .ok_or("second captured link missing")?;
2508        assert_eq!(first.file_id, second.file_id);
2509        std::fs::hard_link(host.path().join("a"), host.path().join("z"))?;
2510        let additional = path("z")?;
2511        capture_paths(
2512            &mut checkout,
2513            &[paths[0].clone(), additional.clone()],
2514            &CaptureOptions {
2515                expected_root_identity: capture_root_identity(host.path())?,
2516                source_root: host.path().to_path_buf(),
2517                maximum_paths: 2,
2518                maximum_extent_spans: 16,
2519            },
2520            WorkBudget::UNBOUNDED,
2521            &cancellation,
2522        )
2523        .await?;
2524        let additional_id = checkout
2525            .lookup_no_follow(&additional, WorkBudget::UNBOUNDED, &cancellation)
2526            .await?
2527            .value
2528            .record
2529            .ok_or("additional link missing")?
2530            .file_id;
2531        assert_eq!(additional_id, first.file_id);
2532        std::fs::hard_link(host.path().join("a"), host.path().join("0"))?;
2533        let earlier_alias = path("0")?;
2534        capture_paths(
2535            &mut checkout,
2536            &[paths[0].clone(), earlier_alias.clone()],
2537            &CaptureOptions {
2538                expected_root_identity: capture_root_identity(host.path())?,
2539                source_root: host.path().to_path_buf(),
2540                maximum_paths: 2,
2541                maximum_extent_spans: 16,
2542            },
2543            WorkBudget::UNBOUNDED,
2544            &cancellation,
2545        )
2546        .await?;
2547        let earlier_id = checkout
2548            .lookup_no_follow(&earlier_alias, WorkBudget::UNBOUNDED, &cancellation)
2549            .await?
2550            .value
2551            .record
2552            .ok_or("earlier alias missing")?
2553            .file_id;
2554        assert_eq!(earlier_id, first.file_id);
2555        std::fs::hard_link(host.path().join("a"), host.path().join("c"))?;
2556        let third = path("c")?;
2557        let watch = capture_watch_batch(
2558            &mut checkout,
2559            WatchBatch::Changes {
2560                epoch: WatchEpoch::from_u64(1),
2561                first_sequence: WatchSequence::from_u64(1),
2562                next_sequence: WatchSequence::from_u64(2),
2563                changes: vec![WatchChange::Created(third.clone())],
2564            },
2565            &CaptureOptions {
2566                expected_root_identity: capture_root_identity(host.path())?,
2567                source_root: host.path().to_path_buf(),
2568                maximum_paths: 1,
2569                maximum_extent_spans: 16,
2570            },
2571            WorkBudget::UNBOUNDED,
2572            &cancellation,
2573        )
2574        .await;
2575        assert!(matches!(
2576            watch,
2577            Err(crate::OperationFailure {
2578                error: CaptureError::RescanRequired { .. },
2579                ..
2580            })
2581        ));
2582        assert!(
2583            checkout
2584                .lookup_no_follow(&third, WorkBudget::UNBOUNDED, &cancellation)
2585                .await?
2586                .value
2587                .record
2588                .is_none()
2589        );
2590        std::fs::write(host.path().join("existing"), b"old")?;
2591        let existing = path("existing")?;
2592        capture_paths(
2593            &mut checkout,
2594            std::slice::from_ref(&existing),
2595            &CaptureOptions {
2596                expected_root_identity: capture_root_identity(host.path())?,
2597                source_root: host.path().to_path_buf(),
2598                maximum_paths: 1,
2599                maximum_extent_spans: 16,
2600            },
2601            WorkBudget::UNBOUNDED,
2602            &cancellation,
2603        )
2604        .await?;
2605        let old_id = checkout
2606            .lookup_no_follow(&existing, WorkBudget::UNBOUNDED, &cancellation)
2607            .await?
2608            .value
2609            .record
2610            .ok_or("existing path missing")?
2611            .file_id;
2612        std::fs::remove_file(host.path().join("existing"))?;
2613        std::fs::hard_link(host.path().join("a"), host.path().join("existing"))?;
2614        let watch = capture_watch_batch(
2615            &mut checkout,
2616            WatchBatch::Changes {
2617                epoch: WatchEpoch::from_u64(1),
2618                first_sequence: WatchSequence::from_u64(2),
2619                next_sequence: WatchSequence::from_u64(3),
2620                changes: vec![WatchChange::Created(existing.clone())],
2621            },
2622            &CaptureOptions {
2623                expected_root_identity: capture_root_identity(host.path())?,
2624                source_root: host.path().to_path_buf(),
2625                maximum_paths: 1,
2626                maximum_extent_spans: 16,
2627            },
2628            WorkBudget::UNBOUNDED,
2629            &cancellation,
2630        )
2631        .await;
2632        assert!(matches!(
2633            watch,
2634            Err(crate::OperationFailure {
2635                error: CaptureError::RescanRequired { .. },
2636                ..
2637            })
2638        ));
2639        assert_eq!(
2640            checkout
2641                .lookup_no_follow(&existing, WorkBudget::UNBOUNDED, &cancellation)
2642                .await?
2643                .value
2644                .record
2645                .ok_or("replacement changed checkout")?
2646                .file_id,
2647            old_id
2648        );
2649        Ok(())
2650    }
2651
2652    #[tokio::test]
2653    async fn baseline_path_limit_stops_unbounded_traversal()
2654    -> Result<(), Box<dyn std::error::Error>> {
2655        let cancellation = CancellationToken::new();
2656        let fs = Fs::memory();
2657        let volume = fs
2658            .create_volume_with_id(
2659                VolumeId::from_bytes([94; 16]),
2660                VolumeConfig::portable(Lifecycle::Ephemeral),
2661                WorkBudget::UNBOUNDED,
2662                &cancellation,
2663            )
2664            .await?
2665            .value;
2666        let mut checkout = volume
2667            .checkout(
2668                GenerationSelector::Head,
2669                CheckoutMode {
2670                    access: AccessMode::ReadWrite,
2671                    consistency: ConsistencyMode::Pinned,
2672                    mutations: MutationMode::PrivateOverlay,
2673                },
2674                WorkBudget::UNBOUNDED,
2675                &cancellation,
2676            )
2677            .await?
2678            .value;
2679        let host = tempfile::tempdir()?;
2680        for index in 0..3 {
2681            std::fs::write(host.path().join(format!("file-{index}")), b"x")?;
2682        }
2683        let result = capture_baseline(
2684            &mut checkout,
2685            &CaptureOptions {
2686                expected_root_identity: capture_root_identity(host.path())?,
2687                source_root: host.path().to_path_buf(),
2688                maximum_paths: 2,
2689                maximum_extent_spans: 16,
2690            },
2691            WorkBudget::UNBOUNDED,
2692            &cancellation,
2693        )
2694        .await;
2695        let Err(error) = result else {
2696            return Err("path cap unexpectedly permitted traversal".into());
2697        };
2698        assert!(matches!(error.error, CaptureError::InvalidOptions));
2699        assert!(error.work.items_examined <= 2);
2700        Ok(())
2701    }
2702
2703    #[tokio::test]
2704    #[allow(clippy::too_many_lines)]
2705    async fn explicit_materialization_and_capture_round_trip_sparse_checkout()
2706    -> Result<(), Box<dyn std::error::Error>> {
2707        Box::pin(explicit_materialization_and_capture_round_trip_sparse_checkout_inner()).await
2708    }
2709
2710    #[allow(clippy::too_many_lines)]
2711    async fn explicit_materialization_and_capture_round_trip_sparse_checkout_inner()
2712    -> Result<(), Box<dyn std::error::Error>> {
2713        let limits = VolumeLimits::default();
2714        let config = VolumeConfig {
2715            profile: FilesystemProfile::Portable,
2716            concurrency: ConcurrencyMode::Optimistic,
2717            lifecycle: Lifecycle::Ephemeral,
2718            case_sensitivity: CaseSensitivity::Sensitive,
2719            unicode: UnicodePolicy::Preserve,
2720            symbolic_links: true,
2721            hard_links: true,
2722            sparse_files: true,
2723            limits,
2724        };
2725        let cancellation = CancellationToken::new();
2726        let fs = Fs::memory();
2727        let volume = fs
2728            .create_volume_with_id(
2729                VolumeId::from_bytes([91; 16]),
2730                config,
2731                WorkBudget::UNBOUNDED,
2732                &cancellation,
2733            )
2734            .await?
2735            .value;
2736        let mut checkout = volume
2737            .checkout(
2738                GenerationSelector::Head,
2739                CheckoutMode {
2740                    access: AccessMode::ReadWrite,
2741                    consistency: ConsistencyMode::Pinned,
2742                    mutations: MutationMode::PrivateOverlay,
2743                },
2744                WorkBudget::UNBOUNDED,
2745                &cancellation,
2746            )
2747            .await?
2748            .value;
2749        let file = NamespacePath::new(
2750            vec![LogicalName::new(
2751                NameEncoding::Utf8,
2752                b"sparse.bin".to_vec(),
2753                limits.maximum_component_bytes,
2754            )?],
2755            limits,
2756        )?;
2757        checkout
2758            .create_file(
2759                file.clone(),
2760                Bytes::from_static(b"head"),
2761                WorkBudget::UNBOUNDED,
2762                &cancellation,
2763            )
2764            .await?;
2765        checkout
2766            .resize_file(
2767                file.clone(),
2768                1024 * 1024,
2769                WorkBudget::UNBOUNDED,
2770                &cancellation,
2771            )
2772            .await?;
2773        checkout
2774            .write_file(
2775                file.clone(),
2776                1024 * 1024 - 4,
2777                Bytes::from_static(b"tail"),
2778                WorkBudget::UNBOUNDED,
2779                &cancellation,
2780            )
2781            .await?;
2782
2783        let temporary = tempfile::tempdir()?;
2784        let destination = temporary.path().join("view");
2785        std::fs::create_dir(&destination)?;
2786        let materialized = Box::pin(materialize_checkout(
2787            &mut checkout,
2788            &MaterializeOptions {
2789                destination: destination.clone(),
2790                maximum_directory_entries: 16,
2791                maximum_extent_spans: 16,
2792                transfer_bytes: 64 * 1024,
2793            },
2794            WorkBudget::UNBOUNDED,
2795            &cancellation,
2796        ))
2797        .await
2798        .map_err(|error| std::io::Error::other(format!("sparse materialization: {error}")))?;
2799        assert_eq!(materialized.value.files, 1);
2800        let host_file = destination.join("sparse.bin");
2801        assert_eq!(
2802            std::fs::metadata(&host_file)
2803                .map_err(|error| std::io::Error::other(format!("materialized metadata: {error}")))?
2804                .len(),
2805            1024 * 1024
2806        );
2807        let body = std::fs::read(&host_file)
2808            .map_err(|error| std::io::Error::other(format!("materialized read: {error}")))?;
2809        assert_eq!(&body[..4], b"head");
2810        assert_eq!(&body[body.len() - 4..], b"tail");
2811
2812        let sparse_capture = capture_paths(
2813            &mut checkout,
2814            std::slice::from_ref(&file),
2815            &CaptureOptions {
2816                expected_root_identity: capture_root_identity(&destination)?,
2817                source_root: destination.clone(),
2818                maximum_paths: 1,
2819                maximum_extent_spans: 16,
2820            },
2821            WorkBudget::UNBOUNDED,
2822            &cancellation,
2823        )
2824        .await
2825        .map_err(|error| std::io::Error::other(format!("sparse recapture: {error}")))?;
2826        assert!(sparse_capture.value.staged_file_bytes < 1024 * 1024);
2827        let sparse_plan = checkout
2828            .plan_file_extents(
2829                &file,
2830                ByteRange {
2831                    offset: 0,
2832                    length: 1024 * 1024,
2833                },
2834                16,
2835                WorkBudget::UNBOUNDED,
2836                &cancellation,
2837            )
2838            .await?
2839            .value
2840            .ok_or("captured sparse file returned an inline plan")?;
2841        assert!(
2842            sparse_plan
2843                .spans
2844                .iter()
2845                .any(|span| matches!(span.kind, crate::kernel::ExtentKind::Hole))
2846        );
2847
2848        std::fs::write(&host_file, b"captured")?;
2849        let captured = capture_paths(
2850            &mut checkout,
2851            std::slice::from_ref(&file),
2852            &CaptureOptions {
2853                expected_root_identity: capture_root_identity(&destination)?,
2854                source_root: destination.clone(),
2855                maximum_paths: 1,
2856                maximum_extent_spans: 16,
2857            },
2858            WorkBudget::UNBOUNDED,
2859            &cancellation,
2860        )
2861        .await?;
2862        assert_eq!(captured.value.changed_paths, 1);
2863        let read = checkout
2864            .read_file_range(
2865                &file,
2866                ByteRange {
2867                    offset: 0,
2868                    length: 8,
2869                },
2870                WorkBudget::UNBOUNDED,
2871                &cancellation,
2872            )
2873            .await?;
2874        assert_eq!(read.value.bytes.as_ref(), b"captured");
2875
2876        let before_rename = checkout
2877            .lookup_no_follow(&file, WorkBudget::UNBOUNDED, &cancellation)
2878            .await?
2879            .value
2880            .record
2881            .ok_or("captured file disappeared")?;
2882        let renamed = NamespacePath::new(
2883            vec![LogicalName::new(
2884                NameEncoding::Utf8,
2885                b"renamed.bin".to_vec(),
2886                limits.maximum_component_bytes,
2887            )?],
2888            limits,
2889        )?;
2890        std::fs::rename(&host_file, destination.join("renamed.bin"))?;
2891        let watch = capture_watch_batch(
2892            &mut checkout,
2893            WatchBatch::Changes {
2894                epoch: WatchEpoch::from_u64(7),
2895                first_sequence: WatchSequence::from_u64(11),
2896                next_sequence: WatchSequence::from_u64(12),
2897                changes: vec![WatchChange::Renamed {
2898                    from: file.clone(),
2899                    to: renamed.clone(),
2900                }],
2901            },
2902            &CaptureOptions {
2903                expected_root_identity: capture_root_identity(&destination)?,
2904                source_root: destination.clone(),
2905                maximum_paths: 1,
2906                maximum_extent_spans: 16,
2907            },
2908            WorkBudget::UNBOUNDED,
2909            &cancellation,
2910        )
2911        .await?;
2912        assert_eq!(watch.value.next_sequence.get(), 12);
2913        let old = checkout
2914            .lookup_no_follow(&file, WorkBudget::UNBOUNDED, &cancellation)
2915            .await?;
2916        assert!(old.value.record.is_none());
2917        let new = checkout
2918            .lookup_no_follow(&renamed, WorkBudget::UNBOUNDED, &cancellation)
2919            .await?
2920            .value
2921            .record
2922            .ok_or("renamed file disappeared")?;
2923        assert_eq!(new.file_id, before_rename.file_id);
2924
2925        let moved = NamespacePath::new(
2926            vec![LogicalName::new(
2927                NameEncoding::Utf8,
2928                b"moved.bin".to_vec(),
2929                limits.maximum_component_bytes,
2930            )?],
2931            limits,
2932        )?;
2933        std::fs::rename(
2934            destination.join("renamed.bin"),
2935            destination.join("moved.bin"),
2936        )?;
2937        std::fs::write(destination.join("renamed.bin"), b"replacement")?;
2938        capture_watch_batch(
2939            &mut checkout,
2940            WatchBatch::Changes {
2941                epoch: WatchEpoch::from_u64(7),
2942                first_sequence: WatchSequence::from_u64(12),
2943                next_sequence: WatchSequence::from_u64(14),
2944                changes: vec![
2945                    WatchChange::Renamed {
2946                        from: renamed.clone(),
2947                        to: moved.clone(),
2948                    },
2949                    WatchChange::Created(renamed.clone()),
2950                ],
2951            },
2952            &CaptureOptions {
2953                expected_root_identity: capture_root_identity(&destination)?,
2954                source_root: destination.clone(),
2955                maximum_paths: 2,
2956                maximum_extent_spans: 16,
2957            },
2958            WorkBudget::UNBOUNDED,
2959            &cancellation,
2960        )
2961        .await?;
2962        let moved_record = checkout
2963            .lookup_no_follow(&moved, WorkBudget::UNBOUNDED, &cancellation)
2964            .await?
2965            .value
2966            .record
2967            .ok_or("moved file disappeared")?;
2968        let replacement_record = checkout
2969            .lookup_no_follow(&renamed, WorkBudget::UNBOUNDED, &cancellation)
2970            .await?
2971            .value
2972            .record
2973            .ok_or("replacement file disappeared")?;
2974        assert_eq!(moved_record.file_id, before_rename.file_id);
2975        assert_ne!(replacement_record.file_id, before_rename.file_id);
2976        let replacement = checkout
2977            .read_file_range(
2978                &renamed,
2979                ByteRange {
2980                    offset: 0,
2981                    length: 11,
2982                },
2983                WorkBudget::UNBOUNDED,
2984                &cancellation,
2985            )
2986            .await?;
2987        assert_eq!(replacement.value.bytes.as_ref(), b"replacement");
2988
2989        std::fs::write(destination.join("renamed.bin"), b"replaced-again")?;
2990        capture_watch_batch(
2991            &mut checkout,
2992            WatchBatch::Changes {
2993                epoch: WatchEpoch::from_u64(7),
2994                first_sequence: WatchSequence::from_u64(14),
2995                next_sequence: WatchSequence::from_u64(15),
2996                changes: vec![WatchChange::Created(renamed.clone())],
2997            },
2998            &CaptureOptions {
2999                expected_root_identity: capture_root_identity(&destination)?,
3000                source_root: destination.clone(),
3001                maximum_paths: 1,
3002                maximum_extent_spans: 16,
3003            },
3004            WorkBudget::UNBOUNDED,
3005            &cancellation,
3006        )
3007        .await?;
3008        let replaced_again = checkout
3009            .lookup_no_follow(&renamed, WorkBudget::UNBOUNDED, &cancellation)
3010            .await?
3011            .value
3012            .record
3013            .ok_or("standalone create replacement disappeared")?;
3014        assert_ne!(replaced_again.file_id, replacement_record.file_id);
3015
3016        std::fs::write(destination.join("renamed.bin"), b"compound-replacement")?;
3017        capture_watch_batch(
3018            &mut checkout,
3019            WatchBatch::Changes {
3020                epoch: WatchEpoch::from_u64(7),
3021                first_sequence: WatchSequence::from_u64(15),
3022                next_sequence: WatchSequence::from_u64(17),
3023                changes: vec![
3024                    WatchChange::Created(renamed.clone()),
3025                    WatchChange::Modified(renamed.clone()),
3026                ],
3027            },
3028            &CaptureOptions {
3029                expected_root_identity: capture_root_identity(&destination)?,
3030                source_root: destination.clone(),
3031                maximum_paths: 2,
3032                maximum_extent_spans: 16,
3033            },
3034            WorkBudget::UNBOUNDED,
3035            &cancellation,
3036        )
3037        .await?;
3038        let compound_replacement = checkout
3039            .lookup_no_follow(&renamed, WorkBudget::UNBOUNDED, &cancellation)
3040            .await?
3041            .value
3042            .record
3043            .ok_or("compound replacement disappeared")?;
3044        assert_ne!(compound_replacement.file_id, replaced_again.file_id);
3045
3046        let dirty_baseline = capture_baseline(
3047            &mut checkout,
3048            &CaptureOptions {
3049                expected_root_identity: capture_root_identity(&destination)?,
3050                source_root: destination.clone(),
3051                maximum_paths: 16,
3052                maximum_extent_spans: 16,
3053            },
3054            WorkBudget::UNBOUNDED,
3055            &cancellation,
3056        )
3057        .await;
3058        let Err(dirty_baseline) = dirty_baseline else {
3059            return Err("dirty baseline replaced private mutations".into());
3060        };
3061        assert!(matches!(dirty_baseline.error, CaptureError::DirtyCheckout));
3062
3063        seal_checkout(
3064            &mut checkout,
3065            crate::OperationId::new(),
3066            WorkBudget::UNBOUNDED,
3067            &cancellation,
3068        )
3069        .await?;
3070
3071        std::fs::remove_file(destination.join("renamed.bin"))?;
3072        std::fs::remove_file(destination.join("moved.bin"))?;
3073        std::fs::create_dir(destination.join("nested"))?;
3074        std::fs::write(destination.join("nested").join("new.txt"), b"baseline")?;
3075        let baseline = capture_baseline(
3076            &mut checkout,
3077            &CaptureOptions {
3078                expected_root_identity: capture_root_identity(&destination)?,
3079                source_root: destination.clone(),
3080                maximum_paths: 16,
3081                maximum_extent_spans: 16,
3082            },
3083            WorkBudget::UNBOUNDED,
3084            &cancellation,
3085        )
3086        .await?;
3087        assert_eq!(baseline.value.examined_paths, 4);
3088        let removed = checkout
3089            .lookup_no_follow(&renamed, WorkBudget::UNBOUNDED, &cancellation)
3090            .await?;
3091        assert!(removed.value.record.is_none());
3092        let removed_moved = checkout
3093            .lookup_no_follow(&moved, WorkBudget::UNBOUNDED, &cancellation)
3094            .await?;
3095        assert!(removed_moved.value.record.is_none());
3096        let nested = PortablePath::parse("/nested/new.txt", limits)?;
3097        let nested = NamespacePath::from_portable(&nested, limits)?;
3098        let nested_read = checkout
3099            .read_file_range(
3100                &nested,
3101                ByteRange {
3102                    offset: 0,
3103                    length: 8,
3104                },
3105                WorkBudget::UNBOUNDED,
3106                &cancellation,
3107            )
3108            .await?;
3109        assert_eq!(nested_read.value.bytes.as_ref(), b"baseline");
3110
3111        let missing =
3112            NamespacePath::from_portable(&PortablePath::parse("/missing.tmp", limits)?, limits)?;
3113        let arrived =
3114            NamespacePath::from_portable(&PortablePath::parse("/arrived.txt", limits)?, limits)?;
3115        std::fs::write(destination.join("arrived.txt"), b"arrival")?;
3116        capture_watch_batch(
3117            &mut checkout,
3118            WatchBatch::Changes {
3119                epoch: WatchEpoch::from_u64(8),
3120                first_sequence: WatchSequence::from_u64(0),
3121                next_sequence: WatchSequence::from_u64(1),
3122                changes: vec![WatchChange::Renamed {
3123                    from: missing,
3124                    to: arrived.clone(),
3125                }],
3126            },
3127            &CaptureOptions {
3128                expected_root_identity: capture_root_identity(&destination)?,
3129                source_root: destination.clone(),
3130                maximum_paths: 1,
3131                maximum_extent_spans: 16,
3132            },
3133            WorkBudget::UNBOUNDED,
3134            &cancellation,
3135        )
3136        .await?;
3137        assert_eq!(
3138            checkout
3139                .read_file_range(
3140                    &arrived,
3141                    ByteRange {
3142                        offset: 0,
3143                        length: 7,
3144                    },
3145                    WorkBudget::UNBOUNDED,
3146                    &cancellation,
3147                )
3148                .await?
3149                .value
3150                .bytes
3151                .as_ref(),
3152            b"arrival"
3153        );
3154
3155        let prior_arrival = checkout
3156            .lookup_no_follow(&arrived, WorkBudget::UNBOUNDED, &cancellation)
3157            .await?
3158            .value
3159            .record
3160            .ok_or("arrival disappeared")?;
3161        let temporary_name =
3162            NamespacePath::from_portable(&PortablePath::parse("/arrival.tmp", limits)?, limits)?;
3163        std::fs::write(destination.join("arrival.tmp"), b"replacement-arrival")?;
3164        std::fs::remove_file(destination.join("arrived.txt"))?;
3165        std::fs::rename(
3166            destination.join("arrival.tmp"),
3167            destination.join("arrived.txt"),
3168        )?;
3169        capture_watch_batch(
3170            &mut checkout,
3171            WatchBatch::Changes {
3172                epoch: WatchEpoch::from_u64(8),
3173                first_sequence: WatchSequence::from_u64(1),
3174                next_sequence: WatchSequence::from_u64(3),
3175                changes: vec![
3176                    WatchChange::Created(temporary_name.clone()),
3177                    WatchChange::Renamed {
3178                        from: temporary_name,
3179                        to: arrived.clone(),
3180                    },
3181                ],
3182            },
3183            &CaptureOptions {
3184                expected_root_identity: capture_root_identity(&destination)?,
3185                source_root: destination.clone(),
3186                maximum_paths: 2,
3187                maximum_extent_spans: 16,
3188            },
3189            WorkBudget::UNBOUNDED,
3190            &cancellation,
3191        )
3192        .await?;
3193        let replaced_arrival = checkout
3194            .lookup_no_follow(&arrived, WorkBudget::UNBOUNDED, &cancellation)
3195            .await?
3196            .value
3197            .record
3198            .ok_or("replacement arrival disappeared")?;
3199        assert_ne!(replaced_arrival.file_id, prior_arrival.file_id);
3200
3201        let created_and_modified = NamespacePath::from_portable(
3202            &PortablePath::parse("/created-and-modified.txt", limits)?,
3203            limits,
3204        )?;
3205        std::fs::write(
3206            destination.join("created-and-modified.txt"),
3207            b"created-and-modified",
3208        )?;
3209        capture_watch_batch(
3210            &mut checkout,
3211            WatchBatch::Changes {
3212                epoch: WatchEpoch::from_u64(8),
3213                first_sequence: WatchSequence::from_u64(3),
3214                next_sequence: WatchSequence::from_u64(5),
3215                changes: vec![
3216                    WatchChange::Created(created_and_modified.clone()),
3217                    WatchChange::Modified(created_and_modified.clone()),
3218                ],
3219            },
3220            &CaptureOptions {
3221                expected_root_identity: capture_root_identity(&destination)?,
3222                source_root: destination.clone(),
3223                maximum_paths: 2,
3224                maximum_extent_spans: 16,
3225            },
3226            WorkBudget::UNBOUNDED,
3227            &cancellation,
3228        )
3229        .await?;
3230        assert_eq!(
3231            checkout
3232                .read_file_range(
3233                    &created_and_modified,
3234                    ByteRange {
3235                        offset: 0,
3236                        length: 20,
3237                    },
3238                    WorkBudget::UNBOUNDED,
3239                    &cancellation,
3240                )
3241                .await?
3242                .value
3243                .bytes
3244                .as_ref(),
3245            b"created-and-modified"
3246        );
3247
3248        let expected_root_identity = capture_root_identity(&destination)?;
3249        let displaced = temporary.path().join("displaced-view");
3250        std::fs::rename(&destination, &displaced)?;
3251        std::fs::create_dir(&destination)?;
3252        std::fs::write(destination.join("arrived.txt"), b"untrusted-root")?;
3253        let swapped = capture_paths(
3254            &mut checkout,
3255            std::slice::from_ref(&arrived),
3256            &CaptureOptions {
3257                source_root: destination,
3258                expected_root_identity,
3259                maximum_paths: 1,
3260                maximum_extent_spans: 16,
3261            },
3262            WorkBudget::UNBOUNDED,
3263            &cancellation,
3264        )
3265        .await;
3266        let Err(swapped) = swapped else {
3267            return Err("capture accepted a replacement root".into());
3268        };
3269        assert!(matches!(swapped.error, CaptureError::RootChanged));
3270        Ok(())
3271    }
3272
3273    #[tokio::test]
3274    async fn coalesced_parent_child_removals_capture_deepest_first()
3275    -> Result<(), Box<dyn std::error::Error>> {
3276        let config = VolumeConfig::portable(Lifecycle::Ephemeral);
3277        let limits = config.limits;
3278        let cancellation = CancellationToken::new();
3279        let fs = Fs::memory();
3280        let volume = fs
3281            .create_volume(config, WorkBudget::UNBOUNDED, &cancellation)
3282            .await?
3283            .value;
3284        let mut checkout = volume
3285            .checkout(
3286                GenerationSelector::Head,
3287                CheckoutMode {
3288                    access: AccessMode::ReadWrite,
3289                    consistency: ConsistencyMode::Pinned,
3290                    mutations: MutationMode::PrivateOverlay,
3291                },
3292                WorkBudget::UNBOUNDED,
3293                &cancellation,
3294            )
3295            .await?
3296            .value;
3297        let temporary = tempfile::tempdir()?;
3298        let root = temporary.path().join("capture");
3299        std::fs::create_dir(&root)?;
3300        let options = CaptureOptions {
3301            expected_root_identity: capture_root_identity(&root)?,
3302            source_root: root.clone(),
3303            maximum_paths: 8,
3304            maximum_extent_spans: 8,
3305        };
3306        capture_baseline(
3307            &mut checkout,
3308            &options,
3309            WorkBudget::UNBOUNDED,
3310            &cancellation,
3311        )
3312        .await?;
3313        let parent = portable_path(limits, &[b"parent"])?;
3314        let child = portable_path(limits, &[b"parent", b"child.txt"])?;
3315        std::fs::create_dir(root.join("parent"))?;
3316        std::fs::write(root.join("parent").join("child.txt"), b"child")?;
3317        capture_watch_batch(
3318            &mut checkout,
3319            WatchBatch::Changes {
3320                epoch: WatchEpoch::from_u64(1),
3321                first_sequence: WatchSequence::from_u64(0),
3322                next_sequence: WatchSequence::from_u64(1),
3323                changes: vec![WatchChange::Created(parent.clone())],
3324            },
3325            &options,
3326            WorkBudget::UNBOUNDED,
3327            &cancellation,
3328        )
3329        .await?;
3330        assert!(
3331            checkout
3332                .lookup_no_follow(&child, WorkBudget::UNBOUNDED, &cancellation)
3333                .await?
3334                .value
3335                .record
3336                .is_some()
3337        );
3338        std::fs::remove_file(root.join("parent").join("child.txt"))?;
3339        std::fs::remove_dir(root.join("parent"))?;
3340        capture_watch_batch(
3341            &mut checkout,
3342            WatchBatch::Changes {
3343                epoch: WatchEpoch::from_u64(1),
3344                first_sequence: WatchSequence::from_u64(1),
3345                next_sequence: WatchSequence::from_u64(3),
3346                changes: vec![
3347                    WatchChange::Removed(parent.clone()),
3348                    WatchChange::Removed(child.clone()),
3349                ],
3350            },
3351            &options,
3352            WorkBudget::UNBOUNDED,
3353            &cancellation,
3354        )
3355        .await?;
3356        assert!(
3357            checkout
3358                .lookup_no_follow(&parent, WorkBudget::UNBOUNDED, &cancellation)
3359                .await?
3360                .value
3361                .record
3362                .is_none()
3363        );
3364        Ok(())
3365    }
3366
3367    // FSEvents can replay creation hints for paths that existed before the
3368    // watcher's baseline. Stale created hints must not replace a directory
3369    // that is still a directory: that remove is illegal while the directory
3370    // has children, and hints only select which paths to authenticate.
3371    #[tokio::test]
3372    async fn stale_created_hints_for_existing_directory_capture_exactly()
3373    -> Result<(), Box<dyn std::error::Error>> {
3374        let config = VolumeConfig::portable(Lifecycle::Ephemeral);
3375        let limits = config.limits;
3376        let cancellation = CancellationToken::new();
3377        let fs = Fs::memory();
3378        let volume = fs
3379            .create_volume(config, WorkBudget::UNBOUNDED, &cancellation)
3380            .await?
3381            .value;
3382        let mut checkout = volume
3383            .checkout(
3384                GenerationSelector::Head,
3385                CheckoutMode {
3386                    access: AccessMode::ReadWrite,
3387                    consistency: ConsistencyMode::Pinned,
3388                    mutations: MutationMode::PrivateOverlay,
3389                },
3390                WorkBudget::UNBOUNDED,
3391                &cancellation,
3392            )
3393            .await?
3394            .value;
3395        let nested = portable_path(limits, &[b"nested"])?;
3396        let child = portable_path(limits, &[b"nested", b"child.txt"])?;
3397        let created = portable_path(limits, &[b"watch.txt"])?;
3398
3399        let temporary = tempfile::tempdir()?;
3400        std::fs::create_dir(temporary.path().join("nested"))?;
3401        std::fs::write(temporary.path().join("nested/child.txt"), b"child")?;
3402        let options = CaptureOptions {
3403            source_root: temporary.path().to_path_buf(),
3404            expected_root_identity: capture_root_identity(temporary.path())?,
3405            maximum_paths: 16,
3406            maximum_extent_spans: 16,
3407        };
3408        capture_baseline(
3409            &mut checkout,
3410            &options,
3411            WorkBudget::UNBOUNDED,
3412            &cancellation,
3413        )
3414        .await?;
3415        let directory_before = checkout
3416            .lookup_no_follow(&nested, WorkBudget::UNBOUNDED, &cancellation)
3417            .await?
3418            .value
3419            .record
3420            .ok_or("captured directory disappeared")?;
3421
3422        std::fs::write(temporary.path().join("watch.txt"), b"created")?;
3423        capture_watch_batch(
3424            &mut checkout,
3425            WatchBatch::Changes {
3426                epoch: WatchEpoch::from_u64(1),
3427                first_sequence: WatchSequence::from_u64(0),
3428                next_sequence: WatchSequence::from_u64(6),
3429                changes: vec![
3430                    WatchChange::Created(child.clone()),
3431                    WatchChange::MetadataChanged(child.clone()),
3432                    WatchChange::Created(nested.clone()),
3433                    WatchChange::MetadataChanged(nested.clone()),
3434                    WatchChange::Created(created.clone()),
3435                    WatchChange::Modified(created.clone()),
3436                ],
3437            },
3438            &options,
3439            WorkBudget::UNBOUNDED,
3440            &cancellation,
3441        )
3442        .await?;
3443        let directory_after = checkout
3444            .lookup_no_follow(&nested, WorkBudget::UNBOUNDED, &cancellation)
3445            .await?
3446            .value
3447            .record
3448            .ok_or("directory disappeared after stale hints")?;
3449        assert_eq!(directory_after.file_id, directory_before.file_id);
3450        for (path, expected) in [(&child, b"child".as_slice()), (&created, b"created")] {
3451            let read = checkout
3452                .read_file_range(
3453                    path,
3454                    ByteRange {
3455                        offset: 0,
3456                        length: u64::try_from(expected.len())?,
3457                    },
3458                    WorkBudget::UNBOUNDED,
3459                    &cancellation,
3460                )
3461                .await?;
3462            assert_eq!(read.value.bytes.as_ref(), expected);
3463        }
3464        Ok(())
3465    }
3466
3467    #[tokio::test]
3468    async fn windows_created_and_modified_hint_captures_one_new_file()
3469    -> Result<(), Box<dyn std::error::Error>> {
3470        let mut config = VolumeConfig::portable(Lifecycle::Ephemeral);
3471        config.profile = FilesystemProfile::Windows;
3472        let limits = config.limits;
3473        let cancellation = CancellationToken::new();
3474        let fs = Fs::memory();
3475        let volume = fs
3476            .create_volume(config, WorkBudget::UNBOUNDED, &cancellation)
3477            .await?
3478            .value;
3479        let mut checkout = volume
3480            .checkout(
3481                GenerationSelector::Head,
3482                CheckoutMode {
3483                    access: AccessMode::ReadWrite,
3484                    consistency: ConsistencyMode::Pinned,
3485                    mutations: MutationMode::PrivateOverlay,
3486                },
3487                WorkBudget::UNBOUNDED,
3488                &cancellation,
3489            )
3490            .await?
3491            .value;
3492        let name = "watch.txt"
3493            .encode_utf16()
3494            .flat_map(u16::to_le_bytes)
3495            .collect();
3496        let path = NamespacePath::new(
3497            vec![LogicalName::new(
3498                NameEncoding::WindowsUtf16Le,
3499                name,
3500                limits.maximum_component_bytes,
3501            )?],
3502            limits,
3503        )?;
3504        let temporary = tempfile::tempdir()?;
3505        std::fs::write(temporary.path().join("watch.txt"), b"created")?;
3506        capture_watch_batch(
3507            &mut checkout,
3508            WatchBatch::Changes {
3509                epoch: WatchEpoch::from_u64(1),
3510                first_sequence: WatchSequence::from_u64(0),
3511                next_sequence: WatchSequence::from_u64(2),
3512                changes: vec![
3513                    WatchChange::Created(path.clone()),
3514                    WatchChange::Modified(path.clone()),
3515                ],
3516            },
3517            &CaptureOptions {
3518                source_root: temporary.path().to_path_buf(),
3519                expected_root_identity: capture_root_identity(temporary.path())?,
3520                maximum_paths: 16,
3521                maximum_extent_spans: 16,
3522            },
3523            WorkBudget::UNBOUNDED,
3524            &cancellation,
3525        )
3526        .await?;
3527        let read = checkout
3528            .read_file_range(
3529                &path,
3530                ByteRange {
3531                    offset: 0,
3532                    length: 7,
3533                },
3534                WorkBudget::UNBOUNDED,
3535                &cancellation,
3536            )
3537            .await?;
3538        assert_eq!(read.value.bytes.as_ref(), b"created");
3539        Ok(())
3540    }
3541
3542    #[cfg(unix)]
3543    #[tokio::test]
3544    #[allow(clippy::too_many_lines)]
3545    async fn posix_fifo_and_socket_materialize_and_capture_exactly()
3546    -> Result<(), Box<dyn std::error::Error>> {
3547        use std::os::unix::fs::FileTypeExt;
3548
3549        let limits = VolumeLimits::default();
3550        let config = VolumeConfig {
3551            profile: FilesystemProfile::Posix,
3552            concurrency: ConcurrencyMode::Optimistic,
3553            lifecycle: Lifecycle::Ephemeral,
3554            case_sensitivity: CaseSensitivity::Sensitive,
3555            unicode: UnicodePolicy::Preserve,
3556            symbolic_links: true,
3557            hard_links: true,
3558            sparse_files: true,
3559            limits,
3560        };
3561        let cancellation = CancellationToken::new();
3562        let fs = Fs::memory();
3563        let source = fs
3564            .create_volume_with_id(
3565                VolumeId::from_bytes([92; 16]),
3566                config,
3567                WorkBudget::UNBOUNDED,
3568                &cancellation,
3569            )
3570            .await?
3571            .value;
3572        let mut source_checkout = source
3573            .checkout(
3574                GenerationSelector::Head,
3575                CheckoutMode {
3576                    access: AccessMode::ReadWrite,
3577                    consistency: ConsistencyMode::Pinned,
3578                    mutations: MutationMode::PrivateOverlay,
3579                },
3580                WorkBudget::UNBOUNDED,
3581                &cancellation,
3582            )
3583            .await?
3584            .value;
3585        let fifo = NamespacePath::new(
3586            vec![LogicalName::new(
3587                NameEncoding::PosixBytes,
3588                b"pipe".to_vec(),
3589                limits.maximum_component_bytes,
3590            )?],
3591            limits,
3592        )?;
3593        let socket = NamespacePath::new(
3594            vec![LogicalName::new(
3595                NameEncoding::PosixBytes,
3596                b"socket".to_vec(),
3597                limits.maximum_component_bytes,
3598            )?],
3599            limits,
3600        )?;
3601        source_checkout
3602            .create_empty_special(
3603                fifo.clone(),
3604                crate::kernel::FileKind::Fifo,
3605                WorkBudget::UNBOUNDED,
3606                &cancellation,
3607            )
3608            .await?;
3609        source_checkout
3610            .create_empty_special(
3611                socket.clone(),
3612                crate::kernel::FileKind::Socket,
3613                WorkBudget::UNBOUNDED,
3614                &cancellation,
3615            )
3616            .await?;
3617
3618        let temporary = tempfile::tempdir()?;
3619        let destination = temporary.path().join("special-view");
3620        std::fs::create_dir(&destination)?;
3621        let materialized = Box::pin(materialize_checkout(
3622            &mut source_checkout,
3623            &MaterializeOptions {
3624                destination: destination.clone(),
3625                maximum_directory_entries: 16,
3626                maximum_extent_spans: 16,
3627                transfer_bytes: 64 * 1024,
3628            },
3629            WorkBudget::UNBOUNDED,
3630            &cancellation,
3631        ))
3632        .await?;
3633        assert_eq!(materialized.value.special_files, 2);
3634        assert!(
3635            std::fs::symlink_metadata(destination.join("pipe"))?
3636                .file_type()
3637                .is_fifo()
3638        );
3639        assert!(
3640            std::fs::symlink_metadata(destination.join("socket"))?
3641                .file_type()
3642                .is_socket()
3643        );
3644
3645        let captured = fs
3646            .create_volume_with_id(
3647                VolumeId::from_bytes([93; 16]),
3648                config,
3649                WorkBudget::UNBOUNDED,
3650                &cancellation,
3651            )
3652            .await?
3653            .value;
3654        let mut captured_checkout = captured
3655            .checkout(
3656                GenerationSelector::Head,
3657                CheckoutMode {
3658                    access: AccessMode::ReadWrite,
3659                    consistency: ConsistencyMode::Pinned,
3660                    mutations: MutationMode::PrivateOverlay,
3661                },
3662                WorkBudget::UNBOUNDED,
3663                &cancellation,
3664            )
3665            .await?
3666            .value;
3667        let receipt = capture_baseline(
3668            &mut captured_checkout,
3669            &CaptureOptions {
3670                expected_root_identity: capture_root_identity(&destination)?,
3671                source_root: destination,
3672                maximum_paths: 2,
3673                maximum_extent_spans: 16,
3674            },
3675            WorkBudget::UNBOUNDED,
3676            &cancellation,
3677        )
3678        .await?;
3679        assert_eq!(receipt.value.changed_paths, 2);
3680        assert_eq!(
3681            captured_checkout
3682                .lookup_no_follow(&fifo, WorkBudget::UNBOUNDED, &cancellation)
3683                .await?
3684                .value
3685                .record
3686                .ok_or("captured FIFO is absent")?
3687                .kind,
3688            crate::kernel::FileKind::Fifo
3689        );
3690        assert_eq!(
3691            captured_checkout
3692                .lookup_no_follow(&socket, WorkBudget::UNBOUNDED, &cancellation)
3693                .await?
3694                .value
3695                .record
3696                .ok_or("captured socket is absent")?
3697                .kind,
3698            crate::kernel::FileKind::Socket
3699        );
3700        Ok(())
3701    }
3702}