fsk 0.0.3

Build cross-platform virtual file systems with ease
Documentation

FSK

Build cross-platform virtual file systems with ease

  • macOS: FSKit extension with an install-once Swift bridge.
  • Linux: multithreaded kernel FUSE adapter.
  • Windows: native ProjFS provider for lazy virtual and network trees.
  • Rust: Filesystem and NetworkFilesystem: Filesystem traits.
  • Native access: opt-in borrowed FSKit, FUSE, and Windows event contexts.

The common API stays deliberately small. Backends identify items with stable 64-bit inode values, stream directory entries into a caller-owned sink, and read directly into caller-provided buffers. Names are arbitrary bytes on Unix and WTF-8 on Windows, preserving Windows UTF-16 names including unpaired surrogates.

The published fsk crate deliberately implements neither Filesystem nor NetworkFilesystem. It does not choose an inode index, cache, locking model, transaction boundary, consistency policy, or storage engine for downstream users. The workspace's memory and network backends are examples only; copy their ideas or replace them entirely.

Platform support

Platform Native layer Model Installation
macOS 26+ FSKit Full virtual filesystem; lazy network support Install and authorize FSK.app once
Linux FUSE kernel protocol through fuser Full virtual filesystem; lazy network support FUSE must be available
Windows 10 1809+ ProjFS Lazy projected tree with Windows-managed local cache Enable the Projected File System optional component

ProjFS intentionally merges projected entries with local files. Writes and hydrated content live in its virtualization root, while providers receive native notifications through the raw hook. This is a particularly good fit for source-control, cloud, content-addressed, and other network-backed trees. A fully synthetic Windows filesystem with provider-owned write semantics would use WinFsp; FSK exposes the Windows-native callback context in a form that does not prevent a WinFsp adapter from being added without changing the common model.

Implement a filesystem

use fsk::{Filesystem, MountSession};

struct MyCoolFilesystem;

impl Filesystem for MyCoolFilesystem {
    fn statfs(&self) -> fsk::Result<fsk::StatFs> {
        todo!()
    }

    fn metadata(&self, inode: u64) -> fsk::Result<fsk::Metadata> {
        todo!()
    }

    fn lookup(
        &self,
        parent: u64,
        name: &[u8],
    ) -> fsk::Result<(u64, fsk::FileType)> {
        todo!()
    }

    fn read_directory(
        &self,
        inode: u64,
        cookie: u64,
        verifier: u64,
        sink: &mut dyn fsk::DirectorySink,
    ) -> fsk::Result<fsk::ReadDirectoryResult> {
        todo!()
    }

    fn read(
        &self,
        inode: u64,
        offset: u64,
        output: &mut [u8],
    ) -> fsk::Result<usize> {
        todo!()
    }
}

let mount = MountSession::mount(MyCoolFilesystem, "/path/to/mount")?;
// Keep `mount` alive. Dropping it unregisters/unmounts the filesystem.
# Ok::<(), std::io::Error>(())

The required methods are sufficient for a read-only filesystem. Mutation methods default to ENOTSUP. Methods can run concurrently, so implementations must be Send + Sync.

The workspace's memory example is a complete CLI and a deliberately separate implementation:

cargo run -p fsk-memory-backend -- /path/to/mount

Control-C performs an orderly unmount. The same executable builds against the native adapter selected by its target OS.

See examples/README.md for the complete example catalog.

Network filesystems and downloaded files

use fsk::{LocalFile, NetworkFile, NetworkFilesystem};

impl NetworkFilesystem for MyCoolFilesystem {
    fn open_file(
        &self,
        inode: u64,
        _writable: bool,
    ) -> fsk::Result<NetworkFile> {
        if let Some(download) = self.downloaded_file(inode) {
            return Ok(NetworkFile::Local(LocalFile::new(
                download.path,
                download.generation,
            )));
        }
        Ok(NetworkFile::Remote)
    }

    fn close_file(
        &self,
        inode: u64,
        file: NetworkFile,
    ) -> fsk::Result<()> {
        self.download_manager.file_closed(inode, file);
        Ok(())
    }
}

let mount = fsk::MountSession::mount_network(
    MyCoolFilesystem::connect()?,
    "/path/to/mount",
)?;
# Ok::<(), std::io::Error>(())

NetworkFile::Remote keeps reads lazy through Filesystem::read. NetworkFile::Local leases an explicitly downloaded file until close_file, letting the backend track activity and perform its own eviction.

  • macOS reads leased local files with pread, bypassing IPC.
  • Linux reads the local descriptor directly on its FUSE worker.
  • Windows ProjFS manages its virtualization root as the local hydration cache.

Native requests without default overhead

Implementers can inspect platform-specific events without forcing every filesystem through an allocation-heavy universal request object:

use fsk::raw::{
    NativeRequest, Operation, RawEvent, RawEventMask, RawOutcome,
};

impl Filesystem for MyCoolFilesystem {
    // common methods...

    fn raw_event_mask(&self) -> RawEventMask {
        RawEventMask::operation(Operation::Open)
            .union(RawEventMask::operation(Operation::Write))
    }

    fn raw_event(&self, event: RawEvent<'_>) -> RawOutcome {
        match event.native {
            NativeRequest::Linux {
                request,
                unique,
                file_handle,
                flags,
            } => {
                // `request` points to the borrowed fuser::Request.
            }
            NativeRequest::Windows {
                callback_data,
                namespace_context,
                command_id,
                flags,
            } => {
                // Live PRJ_CALLBACK_DATA and virtualization context.
            }
            NativeRequest::MacOS {
                request_id,
                item_id,
                flags,
                payload,
            } => {
                // Exact borrowed bridge payload and stable FSKit identifiers.
            }
        }
        RawOutcome::Continue
    }
}

The mask is fetched once when mounting. When it is empty, adapters take one predictable bit test and do not construct an event or call the hook. Buffers are borrowed in place:

  • Linux exposes the live fuser::Request and read/write buffer.
  • Windows exposes PRJ_CALLBACK_DATA, namespace context, command ID, and ProjFS-aligned hydration buffer.
  • macOS exposes exact bridge payloads, request/item identifiers, and flags. FSKit Objective-C pointers cannot be validly exposed to a separate Rust process, so the bridge never presents unsafe cross-process pointer values.

Raw pointers and buffers are valid only for the synchronous hook call.

Architecture

flowchart LR
    C["Filesystem / NetworkFilesystem"] --> A["Platform adapter"]
    A --> L["Linux FUSE"]
    A --> W["Windows ProjFS"]
    A --> M["macOS FSKit IPC"]
    M --> S["Install-once Swift extension"]

macOS mounts use a mode-0600 Unix socket in the FSK App Group container and a random per-mount capability. The extension pools up to one persistent connection per active CPU. Linux uses multiple FUSE event loops and FUSE_DEV_IOC_CLONE where available. Windows lets ProjFS select or configure native callback workers.

Performance

The hot paths avoid serialization frameworks and per-item calls:

  • caller-owned read buffers and borrowed write buffers;
  • packed directory batches on macOS;
  • reusable thread-local FUSE read buffers on Linux;
  • ProjFS-aligned buffers written directly with PrjWriteFileData on Windows;
  • fixed little-endian IPC frames rather than JSON or Codable;
  • writev for macOS response headers and payloads;
  • stable directory cookies and mutation verifiers;
  • no raw-event allocation or virtual call unless its operation is enabled.

These figures use a benchmark-only immutable Filesystem with no inode index, cache, locking, consistency policy, or storage engine. They measure trait dispatch, caller-owned buffers, directory sinks, and raw-hook plumbing rather than either example implementation. Benchmarks were run on an Apple M1 Pro using release builds:

Benchmark Observed result
Trait-object lookup 435 million operations/s median
128 KiB caller-buffer reads 54.8 GiB/s median
10,001-entry directory sink 466 million entries/s median
Raw hooks disabled Within run-to-run noise of direct dispatch
Authenticated macOS IPC lookup 188 thousand round trips/s median

Changing macOS response writes from two syscalls to vectored I/O improved the authenticated lookup result from a 162–169k baseline to 182–196k round trips/s (roughly 12–15% by median, depending on the run). A round trip is about 5.1–5.5 µs on this machine.

Run the same benchmarks locally:

cargo bench -p fsk --bench throughput
cargo bench -p fsk --bench raw_overhead
cargo test --release -p fsk \
  ipc::tests::benchmark_authenticated_ipc_lookup_roundtrip \
  -- --ignored --nocapture

These isolate backend and dispatch costs. Mounted performance also includes the host kernel, FUSE/FSKit/ProjFS, cache state, and the downstream backend; use fio, Instruments, fs_usage, or platform equivalents for deployment-specific measurements.

Building and testing

Rust requires version 1.85 or later.

cargo test --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings

cargo check --workspace --all-targets \
  --target x86_64-unknown-linux-gnu
cargo check --workspace --all-targets \
  --target x86_64-pc-windows-msvc

The CI matrix runs native Rust tests on macOS, Ubuntu, and Windows. The macOS job also generates the Xcode project and runs the Swift bridge tests.

macOS bridge

macOS additionally requires Xcode 26, XcodeGen, and an Apple team capable of signing com.apple.developer.fskit.fsmodule.

(cd fsk/macos && xcodegen generate)
./fsk/macos/check.sh

Install FSK.app, then enable it under System Settings → General → Login Items & Extensions → File System Extensions. A CLI can query and open the authorization guide:

match fsk::authorization_status()? {
    fsk::AuthorizationStatus::Authorized => {}
    fsk::AuthorizationStatus::NeedsApproval => {
        fsk::open_authorization_app()?;
    }
    fsk::AuthorizationStatus::NotInstalled => {
        eprintln!("Install FSK.app in /Applications");
    }
}
# Ok::<(), std::io::Error>(())

Set FSK_APP_PATH while testing an app outside /Applications.

Windows

Enable Projected File System in “Turn Windows features on or off”, or with an administrator shell:

Enable-WindowsOptionalFeature -Online -FeatureName Client-ProjFS

ProjFS requires an empty or previously initialized virtualization root.

Releases

Tags matching v* build, sign, notarize, staple, and upload FSK.app, then publish the matching fsk crate through crates.io Trusted Publishing.

Configure a crates.io trusted publisher for:

  • GitHub owner: specta-rs
  • repository: fsk
  • workflow: release.yml
  • environment: none

The workflow uses GitHub OIDC and does not require a crates.io API-token secret. The tag must exactly match the crate version, such as v0.0.1.

For macOS signing, configure:

  • BUILD_CERTIFICATE_BASE64
  • P12_PASSWORD
  • KEYCHAIN_PASSWORD
  • APP_PROVISIONING_PROFILE_BASE64
  • EXTENSION_PROVISIONING_PROFILE_BASE64
  • APPLE_TEAM_ID
  • APPLE_ID
  • APPLE_APP_SPECIFIC_PASSWORD

Both App IDs and profiles must include the group.dev.specta.fsk App Group.

FSKit still requires its .appex inside a signed .app; a CLI cannot contain the extension as a raw executable. The companion app is installed once and can serve any number of independent Rust mount processes.