Skip to main content

mount/
shell.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Platform-agnostic shell trait.
3//!
4//! [`PlatformShell`] is the seam where a thin per-platform adapter
5//! (FUSE on Linux, FSKit on macOS, ProjFS / CfAPI on Windows) plugs
6//! into the content-addressed core. The core implements this trait
7//! once, and each platform binding wraps it.
8//!
9//! Conceptually the trait is six pure operations: lookup, read,
10//! write, enumerate, attrs, invalidate. They mirror what every
11//! kernel-side filesystem hook ultimately needs to ask, so they can
12//! be implemented for an in-memory test mount, a Git-backed mount,
13//! a Heddle-state-backed mount, etc.
14
15use std::{
16    ffi::{OsStr, OsString},
17    time::SystemTime,
18};
19
20use objects::object::FileMode;
21
22use crate::error::Result;
23
24/// Identifier for a filesystem node within a single mount session.
25///
26/// Reserved value `1` is the root, mirroring FUSE convention. Beyond
27/// that, the core hands out opaque ids that are stable for the
28/// lifetime of the mount but may be invalidated by [`PlatformShell::invalidate`]
29/// when the underlying state moves.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub struct NodeId(pub u64);
32
33impl NodeId {
34    /// Root inode id. FUSE always starts here.
35    pub const ROOT: NodeId = NodeId(1);
36}
37
38/// What a filesystem entry is, structurally.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum NodeKind {
41    Directory,
42    File,
43    Symlink,
44}
45
46/// A single directory entry, returned from [`PlatformShell::lookup`]
47/// and [`PlatformShell::enumerate`].
48#[derive(Clone, Debug)]
49pub struct Entry {
50    pub node: NodeId,
51    pub name: OsString,
52    pub kind: NodeKind,
53    pub size: u64,
54    /// Unix mode bits, including type. Cached so the platform shell
55    /// can answer `attrs` without a second walk.
56    pub unix_mode: u32,
57}
58
59/// Stat-style attributes for a single node.
60#[derive(Clone, Copy, Debug)]
61pub struct Attrs {
62    pub node: NodeId,
63    pub kind: NodeKind,
64    pub size: u64,
65    pub unix_mode: u32,
66    pub nlink: u32,
67    /// Modification / change times. The mount has no per-blob clock,
68    /// so we report a single fixed timestamp captured when the mount
69    /// was created. This keeps `ls -l` from showing nonsense and
70    /// makes diffs against a stable reference deterministic.
71    pub mtime: SystemTime,
72}
73
74/// Platform-agnostic operations every adapter implements against
75/// a shared core. Names mirror the eventual FUSE callbacks (and the
76/// equivalent FSKit / ProjFS hooks) so the platform layer can be
77/// almost trivial.
78///
79/// ## Write lifecycle
80///
81/// Mount writes flow through three calls:
82///
83/// 1. [`write`](PlatformShell::write) — kernel issues a sequence of
84///    `write(offset, bytes)` calls against an open file. The core
85///    accumulates these in an in-memory hot-tier buffer keyed by
86///    `NodeId`.
87/// 2. [`flush`](PlatformShell::flush) — kernel signals the buffer
88///    can be made durable (mapped to FUSE's `flush` callback, which
89///    fires on `close(2)` and on explicit fsync). The core promotes
90///    the hot buffer to a CAS blob and records `path -> blob_oid` in
91///    the per-thread pending tree. Buffer is dropped.
92/// 3. [`release`](PlatformShell::release) — kernel signals the file
93///    is closed and the inode handle can be retired. The default
94///    contract: identical to flush. FUSE doesn't always issue
95///    `flush` cleanly on every close path, so adapters should call
96///    `release` here too as a belt-and-braces measure.
97///
98/// Implementations MAY also promote a hot buffer opportunistically
99/// (e.g. after an idle window) — this is a safety net for files that
100/// the kernel never explicitly closes.
101pub trait PlatformShell {
102    /// Look up `name` inside `parent`. Returns `None` for ENOENT.
103    fn lookup(&self, parent: NodeId, name: &OsStr) -> Result<Option<Entry>>;
104
105    /// Read up to `buf.len()` bytes from `node`, starting at `offset`.
106    /// Returns the number of bytes actually written into `buf`.
107    fn read(&self, node: NodeId, offset: u64, buf: &mut [u8]) -> Result<usize>;
108
109    /// Write `data` to `node` at `offset`. Returns bytes written.
110    fn write(&self, node: NodeId, offset: u64, data: &[u8]) -> Result<usize>;
111
112    /// List the children of `dir`.
113    fn enumerate(&self, dir: NodeId) -> Result<Vec<Entry>>;
114
115    /// Stat `node`.
116    fn attrs(&self, node: NodeId) -> Result<Attrs>;
117
118    /// Drop any cached identity for `node`. The platform layer calls
119    /// this when the underlying state moves and previously-handed-out
120    /// inode numbers may now point at the wrong content.
121    fn invalidate(&self, node: NodeId) -> Result<()>;
122
123    /// Promote any hot-tier buffer for `node` into a CAS blob. The
124    /// FUSE `flush` callback dispatches here (fires on `close(2)`
125    /// and explicit fsync). Default: no-op for read-only mounts.
126    fn flush(&self, _node: NodeId) -> Result<()> {
127        Ok(())
128    }
129
130    /// Final close of `node`. Adapters call this on FUSE `release`
131    /// so a buffer that survived a missed `flush` still gets
132    /// promoted before the inode handle is retired. Default:
133    /// identical to flush.
134    fn release(&self, node: NodeId) -> Result<()> {
135        self.flush(node)
136    }
137}
138
139/// Convert a Heddle [`FileMode`] into a node kind.
140pub(crate) fn kind_for_mode(mode: FileMode) -> NodeKind {
141    match mode {
142        FileMode::Normal | FileMode::Executable => NodeKind::File,
143        FileMode::Symlink => NodeKind::Symlink,
144    }
145}
146
147/// The unix mode bits for a directory. Trees don't carry a mode of
148/// their own — they're synthesised at materialization time — so we
149/// keep one canonical value here.
150pub(crate) const DIR_UNIX_MODE: u32 = 0o040755;