mount/lib.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Heddle's content-addressed mount.
3//!
4//! `mount` is the platform-agnostic core (and Linux FUSE shell) that
5//! exposes a heddle thread as a directory tree. Reads walk the
6//! Merkle DAG lazily; writes (eventually) flow into a per-thread
7//! overlay that drains to a heddle commit on `heddle capture`.
8//!
9//! The architecture is:
10//!
11//! ```text
12//! PlatformShell trait ← thin platform adapters
13//! (FuseShell, FSKitShell, ProjFsShell, NfsShell)
14//! ↓
15//! ContentAddressedMount ← pure Rust core
16//! ↓
17//! crates/repo + crates/objects (already exists)
18//! ```
19//!
20//! Three of those adapters are per-OS (FUSE on Linux, FSKit on
21//! macOS, ProjFS on Windows). [`NfsShell`] is the universal
22//! fallback: it stands up an in-process NFSv3 server and asks the
23//! host's built-in NFS client to mount it. The CLI's mount
24//! lifecycle prefers the native adapter and falls back to NFS
25//! when the native one is unavailable at runtime.
26//!
27//! See [`PlatformShell`] for the trait every adapter implements,
28//! and [`ContentAddressedMount`] for the heddle-aware core.
29
30pub mod cache;
31pub mod core;
32pub mod error;
33mod pending;
34pub mod shell;
35
36#[cfg(all(target_os = "linux", feature = "fuse"))]
37pub mod fuse;
38
39// Out-of-process FUSE worker (heddle#190 / spike heddle#88).
40// `worker` exports the [`worker::Supervisor`] type the CLI's mount
41// lifecycle uses to spawn `heddle-fuse-worker` subprocesses, plus
42// the [`worker::run_worker`] entrypoint the binary calls. Linux +
43// `fuse` only — the feature gate matches the binary's
44// `required-features`.
45#[cfg(all(target_os = "linux", feature = "fuse"))]
46pub mod worker;
47
48#[cfg(all(target_os = "macos", feature = "fskit"))]
49pub mod fskit;
50
51#[cfg(all(target_os = "windows", feature = "projfs"))]
52pub mod projfs;
53
54#[cfg(feature = "nfs")]
55pub mod nfs;
56
57// Re-export the fuser background-session type so callers (notably the
58// CLI's mount lifecycle and daemon registry) don't have to take a
59// direct fuser dep just to hold onto a live mount.
60#[cfg(all(target_os = "linux", feature = "fuse"))]
61pub use fuser::BackgroundSession;
62
63#[cfg(all(target_os = "macos", feature = "fskit"))]
64pub use crate::fskit::FSKitShell;
65#[cfg(all(target_os = "linux", feature = "fuse"))]
66pub use crate::fuse::FuseShell;
67#[cfg(feature = "nfs")]
68pub use crate::nfs::{NfsSession, NfsShell};
69#[cfg(all(target_os = "windows", feature = "projfs"))]
70pub use crate::projfs::{ProjFsSession, ProjFsShell};
71pub use crate::{
72 cache::{BlobCachePool, BlobCacheStats, DEFAULT_BLOB_CACHE_BYTES},
73 core::{ContentAddressedMount, MountOptions, PrewarmHandle, PrewarmStats, PromotionPolicy},
74 error::{MountError, Result},
75 shell::{AttrUpdate, Attrs, Entry, NodeId, NodeKind, PlatformShell},
76};
77
78#[cfg(test)]
79mod tests;
80
81/// NOT a stable public API. Re-exports the `pub(crate)` witness
82/// substrate so a `compile_fail` doctest can pin the brand-isolation
83/// invariant for Codex PR #217 r2 (`crates/mount/src/pending.rs`
84/// finding `3293832936`). Hidden from rendered docs; consumers must
85/// not depend on this module.
86///
87/// # Brand isolation (post heddle#208 r2)
88///
89/// The substrate brands `Pending`, `Witness`, and `KernelForgetWitness`
90/// with an invariant `'brand` lifetime that's introduced per call via
91/// [`core::Pending::with_brand`]. Witnesses minted under one `'brand`
92/// cannot be passed to methods on a `Pending` carrying a different
93/// `'brand` — the cross-instance bug Codex flagged on r1 stops
94/// type-checking.
95///
96/// The doctest below is the executable proof. It compiles against
97/// the pre-brand substrate (r1; cross-instance use is allowed —
98/// THE BUG) and fails to compile against the post-brand substrate
99/// (r2; brand mismatch — THE FIX).
100///
101/// ```compile_fail
102/// use mount::__pending_substrate_for_doctest::*;
103/// let mut p1 = Pending::default();
104/// let mut p2 = Pending::default();
105/// p1.with_brand(|p1_branded| {
106/// p2.with_brand(|p2_branded| {
107/// // `w` carries p1_branded's brand. Pre-fix it's
108/// // `Witness<'_, LiveNonZero>` (no brand); post-fix it's
109/// // `Witness<'_, 'brand_of_p1, LiveNonZero>`.
110/// let w = p1_branded
111/// .witness_live_nonzero(0)
112/// .expect("doctest never runs — compile_fail");
113/// // Pre-fix: `peek_witness` accepts any `&Witness<'_, S>`.
114/// // Compiles → `compile_fail` assertion fails → RED.
115/// // Post-fix: `peek_witness` on `Pending<'brand_of_p2>`
116/// // wants `&Witness<'_, 'brand_of_p2, S>`. Brand
117/// // mismatch → fails to compile → assertion holds → GREEN.
118/// //
119/// // `peek_witness` takes `&self` + `&Witness` (not consuming)
120/// // so the proof stays orthogonal to the borrow-checker
121/// // constraint introduced by `_borrow: PhantomData<&'p mut ()>`
122/// // on the witness — that's a separate spike-doc question
123/// // the retrofit issues will address, not this PR.
124/// let _ = p2_branded.peek_witness(&w);
125/// });
126/// });
127/// ```
128///
129/// # Constructor bypass (post heddle#208 r3)
130///
131/// r2 brand-isolated `Witness` cross-instance use *inside* `with_brand`,
132/// but the `witness_*` constructors were still callable directly on
133/// `&mut Pending<'brand>`. Because `MountInner` stores
134/// `Pending<'static>`, an internal caller could mint
135/// `Witness<..., 'static, _>` without going through `with_brand` at all
136/// — and two distinct `Pending<'static>` values share the `'static`
137/// brand, so witnesses crossed between them (Codex PR #217 r2 finding
138/// `3293898540`).
139///
140/// r3 closes that hole by moving every `witness_*` (and `peek_witness`)
141/// onto a sealed `BrandedPending<'p, 'brand>` wrapper whose only
142/// constructor is `Pending::with_brand`. The doctest below is the
143/// executable proof. It compiles against r2 (constructors live on
144/// `Pending<'brand>`; direct mint succeeds → THE BUG) and fails to
145/// compile against r3 (constructors are unreachable outside
146/// `with_brand` → THE FIX).
147///
148/// ```compile_fail
149/// use mount::__pending_substrate_for_doctest::*;
150/// let mut p1 = Pending::default();
151/// let mut p2 = Pending::default();
152/// // Pre-r3: `Pending::witness_live_nonzero` exists on `Pending<'brand>`
153/// // so this compiles; `w` ends up `Witness<'_, 'static, _>`.
154/// // Post-r3: `Pending` no longer has any `witness_*` method — this
155/// // call refers to a non-existent name and fails to compile.
156/// let w = p1
157/// .witness_live_nonzero(0)
158/// .expect("doctest never runs — compile_fail");
159/// // Pre-r3: `Pending::peek_witness` accepts `&Witness<'_, 'static, _>`
160/// // (both `p2` and `w` carry the `'static` brand). Compiles →
161/// // the cross-instance bypass exists → `compile_fail`
162/// // assertion fails → RED.
163/// // Post-r3: even if you somehow had a witness in scope, `peek_witness`
164/// // is on `BrandedPending` now, so this also fails to compile.
165/// let _ = p2.peek_witness(&w);
166/// ```
167///
168/// # Witness-gated transitions (heddle#209)
169///
170/// The retrofit issues that consume the substrate (heddle#209 / #210 /
171/// #211 / #212) add FSM-transition methods on
172/// [`crate::pending::BrandedPending`] whose entry-point gating mirrors
173/// the [`crate::pending::BrandedPending::witness_*`] constructors:
174/// they live on `BrandedPending`, which can only be obtained via
175/// [`core::Pending::with_brand`]. Direct callers in `core.rs` (or
176/// future external callers) cannot reach the transitions on
177/// [`core::Pending`] itself.
178///
179/// The doctest below pins that entry-point discipline for the first
180/// retrofitted transition (`transition_to_orphan`, heddle#209). It
181/// compiles only if `Pending::transition_to_orphan` exists as a
182/// directly-callable method — and it doesn't, so the doctest fails
183/// to compile and the `compile_fail` assertion holds (GREEN).
184///
185/// ```compile_fail
186/// use mount::__pending_substrate_for_doctest::*;
187/// let mut p = Pending::default();
188/// // `transition_to_orphan` lives on `BrandedPending`, not on
189/// // `Pending`. A direct call on `Pending` is the bypass shape:
190/// // post-retrofit it fails to compile (the method doesn't exist on
191/// // `Pending`), so the `compile_fail` assertion holds → GREEN.
192/// let _ = p.transition_to_orphan(0);
193/// ```
194///
195/// The same entry-point discipline applies to the witness-gated
196/// kernel-forget retrofit (heddle#211): `kernel_forget_inode` lives
197/// on [`crate::pending::BrandedPending`], not on [`core::Pending`].
198/// A direct call on `Pending` is the bypass shape — pre-retrofit
199/// the forget logic was inlined in `MountInner::invalidate` with no
200/// FSM gate, and the natural drive-by "add a method on `Pending`"
201/// would re-open the same r11 #3 race. The doctest below pins the
202/// post-retrofit discipline: `Pending::kernel_forget_inode` does not
203/// exist as a directly-callable method, so this fails to compile
204/// and the `compile_fail` assertion holds (GREEN).
205///
206/// ```compile_fail
207/// use mount::__pending_substrate_for_doctest::*;
208/// let mut p = Pending::default();
209/// // `kernel_forget_inode` lives on `BrandedPending`, not on
210/// // `Pending`. A direct un-witnessed call on `Pending` is the
211/// // bypass shape that would re-introduce r11 #3 (drop hot[id]
212/// // without first checking the FSM). Post-retrofit the method
213/// // doesn't exist on `Pending`, so the call fails to compile and
214/// // the `compile_fail` assertion holds → GREEN.
215/// let _ = p.kernel_forget_inode(0);
216/// ```
217#[doc(hidden)]
218pub mod __pending_substrate_for_doctest {
219 pub use crate::{
220 core::Pending,
221 pending::{
222 BrandedPending, KernelForgetWitness, Lifecycle, LiveNonZero, LiveZero, Orphan,
223 Released, Witness,
224 },
225 };
226}