Skip to main content

PlatformShell

Trait PlatformShell 

Source
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:

  1. write — kernel issues a sequence of write(offset, bytes) calls against an open file. The core accumulates these in an in-memory hot-tier buffer keyed by NodeId.
  2. flush — kernel signals the buffer can be made durable (mapped to FUSE’s flush callback, which fires on close(2) and on explicit fsync). The core promotes the hot buffer to a CAS blob and records path -> blob_oid in the per-thread pending tree. Buffer is dropped.
  3. release — kernel signals the file is closed and the inode handle can be retired. The default contract: identical to flush. FUSE doesn’t always issue flush cleanly on every close path, so adapters should call release here 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§

Source

fn lookup(&self, parent: NodeId, name: &OsStr) -> Result<Option<Entry>>

Look up name inside parent. Returns None for ENOENT.

Source

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.

Source

fn write(&self, node: NodeId, offset: u64, data: &[u8]) -> Result<usize>

Write data to node at offset. Returns bytes written.

Source

fn enumerate(&self, dir: NodeId) -> Result<Vec<Entry>>

List the children of dir.

Source

fn attrs(&self, node: NodeId) -> Result<Attrs>

Stat node.

Source

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§

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Create a symbolic link named name under parent whose target is the byte-equivalent of target. Returns the new link’s Entry.

Read the target of a symbolic link node. Returns the raw bytes of the link target (which may not be valid UTF-8 on some systems, hence OsString).

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§