pub trait PlatformShell {
Show 18 methods
// Required methods
fn lookup(&self, parent: NodeId, name: &OsStr) -> Result<Option<Entry>>;
fn read(&self, node: NodeId, offset: u64, buf: &mut [u8]) -> Result<usize>;
fn write(&self, node: NodeId, offset: u64, data: &[u8]) -> Result<usize>;
fn enumerate(&self, dir: NodeId) -> Result<Vec<Entry>>;
fn attrs(&self, node: NodeId) -> Result<Attrs>;
fn invalidate(&self, node: NodeId) -> Result<()>;
// Provided methods
fn flush(&self, _node: NodeId) -> Result<()> { ... }
fn release(&self, node: NodeId) -> Result<()> { ... }
fn on_open(&self, _node: NodeId) -> Result<()> { ... }
fn create_file(
&self,
_parent: NodeId,
_name: &OsStr,
_mode: FileMode,
_exclusive: bool,
) -> Result<Entry> { ... }
fn make_dir(&self, _parent: NodeId, _name: &OsStr) -> Result<Entry> { ... }
fn unlink_entry(&self, _parent: NodeId, _name: &OsStr) -> Result<()> { ... }
fn rmdir_entry(&self, _parent: NodeId, _name: &OsStr) -> Result<()> { ... }
fn rename_entry(
&self,
_old_parent: NodeId,
_old_name: &OsStr,
_new_parent: NodeId,
_new_name: &OsStr,
) -> Result<()> { ... }
fn rename_entry_with_options(
&self,
old_parent: NodeId,
old_name: &OsStr,
new_parent: NodeId,
new_name: &OsStr,
_options: RenameOptions,
) -> Result<()> { ... }
fn set_attrs(&self, _node: NodeId, _update: AttrUpdate) -> Result<Attrs> { ... }
fn create_symlink(
&self,
_parent: NodeId,
_name: &OsStr,
_target: &Path,
) -> Result<Entry> { ... }
fn read_link(&self, _node: NodeId) -> Result<OsString> { ... }
}Expand description
Platform-agnostic operations every adapter implements against a shared core. Names mirror the eventual FUSE callbacks (and the equivalent FSKit / ProjFS hooks) so the platform layer can be almost trivial.
§Write lifecycle
Mount writes flow through three calls:
write— kernel issues a sequence ofwrite(offset, bytes)calls against an open file. The core accumulates these in an in-memory hot-tier buffer keyed byNodeId.flush— kernel signals the buffer can be made durable (mapped to FUSE’sflushcallback, which fires onclose(2)and on explicit fsync). The core promotes the hot buffer to a CAS blob and recordspath -> blob_oidin the per-thread pending tree. Buffer is dropped.release— kernel signals the file is closed and the inode handle can be retired. The default contract: identical to flush. FUSE doesn’t always issueflushcleanly on every close path, so adapters should callreleasehere too as a belt-and-braces measure.
Implementations MAY also promote a hot buffer opportunistically (e.g. after an idle window) — this is a safety net for files that the kernel never explicitly closes.
§Platform notes
The three-call write lifecycle above describes the Linux/FUSE
path verbatim — fuser delivers each write(2) syscall as a
write callback, then close(2) triggers flush and release.
FSKit on macOS exposes the same per-write granularity.
On Windows, ProjFS does not intercept individual writes: after a
virtualized file is “hydrated” by the first read, subsequent
writes go straight to NTFS and ProjFS only notifies the provider
after the handle closes. The ProjFS adapter bridges this by
reading the now-fully-hydrated file at close time and synthesizing
a single write(node, 0, full_contents) + flush(node) against
this trait. The hot-tier per-write buffer is therefore a
Linux/FUSE (and FSKit) optimization — implementations of this
trait can rely on the buffer being non-empty only on platforms
that deliver per-write callbacks.
Required Methods§
Sourcefn lookup(&self, parent: NodeId, name: &OsStr) -> Result<Option<Entry>>
fn lookup(&self, parent: NodeId, name: &OsStr) -> Result<Option<Entry>>
Look up name inside parent. Returns None for ENOENT.
Sourcefn read(&self, node: NodeId, offset: u64, buf: &mut [u8]) -> Result<usize>
fn read(&self, node: NodeId, offset: u64, buf: &mut [u8]) -> Result<usize>
Read up to buf.len() bytes from node, starting at offset.
Returns the number of bytes actually written into buf.
Sourcefn write(&self, node: NodeId, offset: u64, data: &[u8]) -> Result<usize>
fn write(&self, node: NodeId, offset: u64, data: &[u8]) -> Result<usize>
Write data to node at offset. Returns bytes written.
Sourcefn invalidate(&self, node: NodeId) -> Result<()>
fn invalidate(&self, node: NodeId) -> Result<()>
Drop any cached identity for node. The platform layer calls
this when the underlying state moves and previously-handed-out
inode numbers may now point at the wrong content.
Provided Methods§
Sourcefn flush(&self, _node: NodeId) -> Result<()>
fn flush(&self, _node: NodeId) -> Result<()>
Promote any hot-tier buffer for node into a CAS blob. The
FUSE flush callback dispatches here (fires on close(2)
and explicit fsync). Default: no-op for read-only mounts.
Lifecycle note: FUSE flush fires on every descriptor close
— including the close of a dup-derived fd — so it can be
invoked multiple times before the last open handle is gone.
Implementations that maintain per-inode “is the directory
entry still gone?” state (orphan tracking) MUST defer the
final clear to Self::release; touching it here would let a
surviving fd’s next write republish the unlinked pathname.
Sourcefn release(&self, node: NodeId) -> Result<()>
fn release(&self, node: NodeId) -> Result<()>
Final close of node. The FUSE release callback dispatches
here; it fires once per open(2) after the last fd derived
from that open is closed. This is the canonical “last close of
the inode” signal — it is the right hook (NOT Self::flush)
for retiring per-inode lifecycle state like orphan-tracking
markers or open-handle refcounts. Default: identical to flush
so shells that do not maintain per-inode lifecycle state
inherit a uniform contract.
Sourcefn on_open(&self, _node: NodeId) -> Result<()>
fn on_open(&self, _node: NodeId) -> Result<()>
Notify the shell that a new open file handle for node has
been minted. FUSE adapters call this on the open / create
callbacks so the shell can maintain a per-inode open-handle
refcount — used to time the Self::release cleanup against
the final close instead of the first one. Default: no-op so
shells without lifecycle state are unaffected.
Sourcefn create_file(
&self,
_parent: NodeId,
_name: &OsStr,
_mode: FileMode,
_exclusive: bool,
) -> Result<Entry>
fn create_file( &self, _parent: NodeId, _name: &OsStr, _mode: FileMode, _exclusive: bool, ) -> Result<Entry>
Create a fresh regular file under parent. Mints a NodeId
for the new file in the writable overlay and returns its
Entry; subsequent write calls
land in the per-thread hot tier.
When exclusive is true (O_CREAT|O_EXCL), the call must
fail with MountError::AlreadyExists if name already
resolves under parent (either in the captured tree or the
pending tier). When exclusive is false, a hit on an
existing entry is returned as-is (same shape as lookup).
Default: MountError::ReadOnly — implementations that
don’t support mutation inherit a uniform errno.
Sourcefn make_dir(&self, _parent: NodeId, _name: &OsStr) -> Result<Entry>
fn make_dir(&self, _parent: NodeId, _name: &OsStr) -> Result<Entry>
Create an empty directory under parent in the overlay.
Returns the new directory’s Entry. Fails with
MountError::AlreadyExists when name already resolves.
Sourcefn unlink_entry(&self, _parent: NodeId, _name: &OsStr) -> Result<()>
fn unlink_entry(&self, _parent: NodeId, _name: &OsStr) -> Result<()>
Delete the file named name under parent. The captured-tree
entry (if any) is tombstoned so lookup /
enumerate skip it; any pending-tier hot
buffer or warm blob for the path is dropped.
Fails with MountError::NotFound if name doesn’t resolve,
or MountError::IsADirectory if it resolves to a directory.
Sourcefn rmdir_entry(&self, _parent: NodeId, _name: &OsStr) -> Result<()>
fn rmdir_entry(&self, _parent: NodeId, _name: &OsStr) -> Result<()>
Remove the empty directory named name under parent. Fails
with MountError::NotADirectory for a file, with
MountError::NotEmpty when the directory still has visible
children (across captured tree + pending tier), or
MountError::NotFound when nothing resolves.
Sourcefn rename_entry(
&self,
_old_parent: NodeId,
_old_name: &OsStr,
_new_parent: NodeId,
_new_name: &OsStr,
) -> Result<()>
fn rename_entry( &self, _old_parent: NodeId, _old_name: &OsStr, _new_parent: NodeId, _new_name: &OsStr, ) -> Result<()>
Atomically rename (old_parent, old_name) to
(new_parent, new_name). Handles both same-directory and
cross-directory cases. Replacing an existing entry of the
same kind is allowed (POSIX semantics); replacing a directory
with a file (or vice-versa) fails with
MountError::IsADirectory / MountError::NotADirectory.
Sourcefn rename_entry_with_options(
&self,
old_parent: NodeId,
old_name: &OsStr,
new_parent: NodeId,
new_name: &OsStr,
_options: RenameOptions,
) -> Result<()>
fn rename_entry_with_options( &self, old_parent: NodeId, old_name: &OsStr, new_parent: NodeId, new_name: &OsStr, _options: RenameOptions, ) -> Result<()>
Same as Self::rename_entry but honours RenameOptions —
in particular no_replace, which atomically refuses the rename
when the destination already resolves. The check + the
directory-entry mutation MUST happen under a single critical
section to avoid a TOCTOU window between the existence check
and the rename itself. Default: ignore options and dispatch to
rename_entry (preserving the existing trait surface for
shells that do not yet support flags).
Sourcefn set_attrs(&self, _node: NodeId, _update: AttrUpdate) -> Result<Attrs>
fn set_attrs(&self, _node: NodeId, _update: AttrUpdate) -> Result<Attrs>
Apply attribute updates to node. Returns the post-update
Attrs so callers can reply without a second getattr
round trip. See AttrUpdate for which fields the overlay
actually persists; unsupported fields are no-ops.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".