Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Backend Implementer’s Guide

This guide walks you through implementing a custom AnyFS backend.


Overview

AnyFS uses layered traits - you implement only what you need:

FsPosix (full POSIX)
   │
FsFuse (FUSE-mountable)
   │
FsFull (std::fs features)
   │
   Fs (basic - 90% of use cases)
   │
FsRead + FsWrite + FsDir (core)

Key properties:

  • Backends accept &Path for all path parameters
  • Backends receive already-resolved paths - FileStorage handles path resolution via pluggable PathResolver (see ADR-033). Default is IterativeResolver for symlink-aware resolution.
  • Backends handle storage only - just store/retrieve bytes at given paths
  • Policy (limits, feature gates) is handled by middleware, not backends
  • Implement only the traits your backend supports
  • Backends must be thread-safe - all trait methods use &self, so backends must use interior mutability (e.g., RwLock, Mutex) for synchronization

Dependency

Depend only on anyfs-backend:

[dependencies]
anyfs-backend = "0.1"

Choosing Which Traits to Implement

Your Backend SupportsImplement
Basic file operationsFs (= FsRead + FsWrite + FsDir)
Links, permissions, syncAdd FsLink, FsPermissions, FsSync, FsStats
Hardlinks, FUSE mountingAdd FsInode → becomes FsFuse
Full POSIX (handles, locks, xattr)Add FsHandles, FsLock, FsXattr → becomes FsPosix

Minimal Backend: Just Fs

#![allow(unused)]
fn main() {
use anyfs_backend::{FsRead, FsWrite, FsDir, FsError, Metadata, DirEntry};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

pub struct MyBackend {
    // Your storage fields
}

// Implement FsRead
impl FsRead for MyBackend {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let path = path.as_ref();
        todo!()
    }

    fn read_to_string(&self, path: &Path) -> Result<String, FsError> {
        let data = self.read(path)?;
        String::from_utf8(data).map_err(|e| FsError::Backend(e.to_string()))
    }

    fn read_range(&self, path: &Path, offset: u64, len: usize) -> Result<Vec<u8>, FsError> {
        todo!()
    }

    fn exists(&self, path: &Path) -> Result<bool, FsError> {
        todo!()
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        todo!()
    }

    fn open_read(&self, path: &Path) -> Result<Box<dyn Read + Send>, FsError> {
        let data = self.read(path)?;
        Ok(Box::new(std::io::Cursor::new(data)))
    }
}

// Implement FsWrite
impl FsWrite for MyBackend {
    fn write(&self, path: &Path, data: &[u8]) -> Result<(), FsError> {
        todo!()
    }

    fn append(&self, path: &Path, data: &[u8]) -> Result<(), FsError> {
        todo!()
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        todo!()
    }

    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> {
        todo!()
    }

    fn copy(&self, from: &Path, to: &Path) -> Result<(), FsError> {
        todo!()
    }

    fn truncate(&self, path: &Path, size: u64) -> Result<(), FsError> {
        todo!()
    }

    fn open_write(&self, path: &Path) -> Result<Box<dyn Write + Send>, FsError> {
        todo!()
    }
}

// Implement FsDir
impl FsDir for MyBackend {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        todo!()
    }

    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        todo!()
    }

    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
        todo!()
    }

    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
        todo!()
    }

    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
        todo!()
    }
}

// MyBackend now implements Fs automatically (blanket impl)!
}

Implementation Steps

Step 1: Pick a Data Model

Your backend needs internal storage. Options:

  • HashMap-based: HashMap<PathBuf, Entry> for simple cases
  • Tree-based: Explicit directory tree structure
  • Database-backed: SQLite, key-value store, etc.

Minimum metadata per entry:

  • File type (file/directory/symlink)
  • Size (for files)
  • Content (for files)
  • Timestamps (optional)
  • Permissions (optional)

Step 2: Implement FsRead (Layer 1)

Start with read operations (easiest):

#![allow(unused)]
fn main() {
impl FsRead for MyBackend {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError>;
    fn read_to_string(&self, path: &Path) -> Result<String, FsError>;
    fn read_range(&self, path: &Path, offset: u64, len: usize) -> Result<Vec<u8>, FsError>;
    fn exists(&self, path: &Path) -> Result<bool, FsError>;
    fn metadata(&self, path: &Path) -> Result<Metadata, FsError>;
    fn open_read(&self, path: &Path) -> Result<Box<dyn Read + Send>, FsError>;
}
}

Streaming implementation options:

For MemoryBackend or similar, you can use std::io::Cursor:

#![allow(unused)]
fn main() {
fn open_read(&self, path: &Path) -> Result<Box<dyn Read + Send>, FsError> {
    let data = self.read(path)?;
    Ok(Box::new(std::io::Cursor::new(data)))
}
}

For VRootFsBackend, return the actual file handle:

#![allow(unused)]
fn main() {
fn open_read(&self, path: &Path) -> Result<Box<dyn Read + Send>, FsError> {
    let file = std::fs::File::open(self.resolve(path)?)?;
    Ok(Box::new(file))
}
}

Step 3: Implement FsWrite (Layer 1)

#![allow(unused)]
fn main() {
impl FsWrite for MyBackend {
    fn write(&self, path: &Path, data: &[u8]) -> Result<(), FsError>;
    fn append(&self, path: &Path, data: &[u8]) -> Result<(), FsError>;
    fn remove_file(&self, path: &Path) -> Result<(), FsError>;
    fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError>;
    fn copy(&self, from: &Path, to: &Path) -> Result<(), FsError>;
    fn truncate(&self, path: &Path, size: u64) -> Result<(), FsError>;
    fn open_write(&self, path: &Path) -> Result<Box<dyn Write + Send>, FsError>;
}
}

Note on truncate:

  • If size < current: discard trailing bytes
  • If size > current: extend with zero bytes
  • Required for FUSE support and editor save operations

Step 4: Implement FsDir (Layer 1)

#![allow(unused)]
fn main() {
impl FsDir for MyBackend {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError>;
    fn create_dir(&self, path: &Path) -> Result<(), FsError>;
    fn create_dir_all(&self, path: &Path) -> Result<(), FsError>;
    fn remove_dir(&self, path: &Path) -> Result<(), FsError>;
    fn remove_dir_all(&self, path: &Path) -> Result<(), FsError>;
}
}

Congratulations! After implementing FsRead, FsWrite, and FsDir, your backend implements Fs automatically (blanket impl). This covers 90% of use cases.


Optional: Layer 2 Traits

Add these if your backend supports the features:

#![allow(unused)]
fn main() {
impl FsLink for MyBackend {
    fn symlink(&self, original: &Path, link: &Path) -> Result<(), FsError>;
    fn hard_link(&self, original: &Path, link: &Path) -> Result<(), FsError>;
    fn read_link(&self, path: &Path) -> Result<PathBuf, FsError>;
    fn symlink_metadata(&self, path: &Path) -> Result<Metadata, FsError>;
}
}
  • Symlinks store a target path as a string
  • Hard links share content with the original (update link count)

FsPermissions

#![allow(unused)]
fn main() {
impl FsPermissions for MyBackend {
    fn set_permissions(&self, path: &Path, perm: Permissions) -> Result<(), FsError>;
}
}

FsSync - Durability

#![allow(unused)]
fn main() {
impl FsSync for MyBackend {
    fn sync(&self) -> Result<(), FsError>;
    fn fsync(&self, path: &Path) -> Result<(), FsError>;
}
}
  • sync(): Flush all pending writes to durable storage
  • fsync(path): Flush pending writes for a specific file
  • MemoryBackend can no-op these (volatile by design)
  • SqliteBackend: PRAGMA wal_checkpoint or connection flush
  • VRootFsBackend: std::fs::File::sync_all()

FsStats - Filesystem Stats

#![allow(unused)]
fn main() {
impl FsStats for MyBackend {
    fn statfs(&self) -> Result<StatFs, FsError>;
}
}

Return filesystem capacity information:

#![allow(unused)]
fn main() {
StatFs {
    total_bytes: 0,      // 0 = unlimited
    used_bytes: ...,
    available_bytes: ...,
    total_inodes: 0,
    used_inodes: ...,
    available_inodes: ...,
    block_size: 4096,
    max_name_len: 255,
}
}

Optional: Layer 3 - FsInode (For FUSE)

Implement FsInode if you need FUSE mounting or inode-based hardlink tracking:

#![allow(unused)]
fn main() {
impl FsInode for MyBackend {
    fn path_to_inode(&self, path: &Path) -> Result<u64, FsError>;
    fn inode_to_path(&self, inode: u64) -> Result<PathBuf, FsError>;
    fn lookup(&self, parent_inode: u64, name: &OsStr) -> Result<u64, FsError>;
    fn metadata_by_inode(&self, inode: u64) -> Result<Metadata, FsError>;
}
}

No blanket/default implementation - you must implement this trait explicitly if you need:

  • FUSE mounting: FUSE operates on inodes, not paths
  • Inode tracking for hardlinks: Two paths share the same inode (note: hard_link() creation is in FsLink)

Level 1: Simple backend (no FsInode)

Don’t implement FsInode. The backend won’t support FUSE mounting. Hardlink creation via FsLink::hard_link() still works, but inode sharing won’t be tracked.

Level 2: Hardlink support

Override path_to_inode so hardlinked paths return the same inode:

#![allow(unused)]
fn main() {
struct Node {
    id: u64,          // Unique node ID (the inode)
    nlink: u64,       // Hard link count
    content: Vec<u8>,
}

struct MemoryBackend {
    next_id: u64,
    nodes: HashMap<u64, Node>,           // inode -> Node
    paths: HashMap<PathBuf, u64>,        // path -> inode
}

impl FsInode for MemoryBackend {
    fn path_to_inode(&self, path: &Path) -> Result<u64, FsError> {
        self.paths.get(path.as_ref())
            .copied()
            .ok_or_else(|| FsError::NotFound { path: path.as_ref().into() })
    }
    // ... implement others
}

impl FsLink for MemoryBackend {
    fn hard_link(&self, original: &Path, link: &Path) -> Result<(), FsError> {
        let inode = self.path_to_inode(&original)?;
        self.paths.insert(link.as_ref().to_path_buf(), inode);
        self.nodes.get_mut(&inode).unwrap().nlink += 1;
        Ok(())
    }
}
}

Level 3: Full FUSE efficiency

Override all 4 methods for O(1) inode operations:

#![allow(unused)]
fn main() {
impl FsInode for SqliteBackend {
    fn path_to_inode(&self, path: &Path) -> Result<u64, FsError> {
        self.conn.query_row(
            "SELECT id FROM nodes WHERE path = ?",
            [path.as_ref().to_string_lossy()],
            |row| Ok(row.get::<_, i64>(0)? as u64),
        ).map_err(|_| FsError::NotFound { path: path.as_ref().into() })
    }

    fn inode_to_path(&self, inode: u64) -> Result<PathBuf, FsError> {
        self.conn.query_row(
            "SELECT path FROM nodes WHERE id = ?",
            [inode as i64],
            |row| Ok(PathBuf::from(row.get::<_, String>(0)?)),
        ).map_err(|_| FsError::NotFound { path: format!("inode:{}", inode).into() })
    }

    fn lookup(&self, parent_inode: u64, name: &OsStr) -> Result<u64, FsError> {
        self.conn.query_row(
            "SELECT id FROM nodes WHERE parent_id = ? AND name = ?",
            params![parent_inode as i64, name.to_string_lossy()],
            |row| Ok(row.get::<_, i64>(0)? as u64),
        ).map_err(|_| FsError::NotFound { path: name.into() })
    }

    fn metadata_by_inode(&self, inode: u64) -> Result<Metadata, FsError> {
        self.conn.query_row(
            "SELECT type, size, nlink, created, modified FROM nodes WHERE id = ?",
            [inode as i64],
            |row| Ok(Metadata {
                inode,
                nlink: row.get(2)?,
                // ...
            }),
        ).map_err(|_| FsError::NotFound { path: format!("inode:{}", inode).into() })
    }
}
}

Summary:

Your BackendImplementResult
Simple (no hardlinks)NothingWorks with defaults
With hardlinksFsInode::path_to_inodeHardlinks work correctly
FUSE-optimizedFull FsInodeMaximum performance

Optional: Layer 4 - POSIX Traits

For full POSIX semantics (file handles, locking, extended attributes):

FsHandles - File Handle Operations

#![allow(unused)]
fn main() {
impl FsHandles for MyBackend {
    fn open(&self, path: &Path, flags: OpenFlags) -> Result<Handle, FsError>;
    fn read_at(&self, handle: Handle, buf: &mut [u8], offset: u64) -> Result<usize, FsError>;
    fn write_at(&self, handle: Handle, data: &[u8], offset: u64) -> Result<usize, FsError>;
    fn close(&self, handle: Handle) -> Result<(), FsError>;
}
}

FsLock - File Locking

#![allow(unused)]
fn main() {
impl FsLock for MyBackend {
    fn lock(&self, handle: Handle, lock: LockType) -> Result<(), FsError>;
    fn try_lock(&self, handle: Handle, lock: LockType) -> Result<bool, FsError>;
    fn unlock(&self, handle: Handle) -> Result<(), FsError>;
}
}

FsXattr - Extended Attributes

#![allow(unused)]
fn main() {
impl FsXattr for MyBackend {
    fn get_xattr(&self, path: &Path, name: &str) -> Result<Vec<u8>, FsError>;
    fn set_xattr(&self, path: &Path, name: &str, value: &[u8]) -> Result<(), FsError>;
    fn remove_xattr(&self, path: &Path, name: &str) -> Result<(), FsError>;
    fn list_xattr(&self, path: &Path) -> Result<Vec<String>, FsError>;
}
}

Note: Most backends don’t need Layer 4. Only implement if you’re wrapping a real filesystem (VRootFsBackend) or building a database that needs full POSIX semantics.


Error Handling

Return appropriate FsError variants:

SituationError
Path doesn’t existFsError::NotFound { path, operation }
Path already existsFsError::AlreadyExists { path, operation }
Expected file, got dirFsError::NotAFile { path }
Expected dir, got fileFsError::NotADirectory { path }
Remove non-empty dirFsError::DirectoryNotEmpty { path }
Internal errorFsError::Backend { message }

What Backends Do NOT Do

ConcernWhere It Lives
Quota enforcementQuota<B> middleware
Feature gatingRestrictions<B> middleware
LoggingTracing<B> middleware
Ergonomic APIFileStorage<B> wrapper

Backends focus on storage. Keep them simple.


Optional Optimizations

Some trait methods have default implementations that work universally but may be suboptimal for specific backends. You can override these for better performance.

Path Canonicalization (FsPath Trait)

The FsPath trait provides canonicalize() and soft_canonicalize() with default implementations that call read_link() and symlink_metadata() per path component.

Default behavior: O(n) calls for a path with n components

When to override:

  • Your backend can resolve paths more efficiently (e.g., SQL query)
  • Your backend delegates to OS (which has optimized syscalls)

SQLite Example - Single Query Resolution:

#![allow(unused)]
fn main() {
impl FsPath for SqliteBackend {
    fn canonicalize(&self, path: &Path) -> Result<PathBuf, FsError> {
        // Resolve entire path in one recursive CTE query
        self.conn.query_row(
            r#"
            WITH RECURSIVE resolve(current, depth) AS (
                SELECT :path, 0
                UNION ALL
                SELECT 
                    CASE WHEN n.type = 'symlink' 
                         THEN n.target 
                         ELSE resolve.current 
                    END,
                    depth + 1
                FROM resolve
                LEFT JOIN nodes n ON n.path = resolve.current
                WHERE n.type = 'symlink' AND depth < 40
            )
            SELECT current FROM resolve ORDER BY depth DESC LIMIT 1
            "#,
            params![path.to_string_lossy()],
            |row| Ok(PathBuf::from(row.get::<_, String>(0)?))
        ).map_err(|_| FsError::NotFound { 
            path: path.into(), 
            operation: "canonicalize" 
        })
    }
}
}

VRootFsBackend Example - OS Delegation:

#![allow(unused)]
fn main() {
impl FsPath for VRootFsBackend {
    fn canonicalize(&self, path: &Path) -> Result<PathBuf, FsError> {
        // Delegate to OS, which uses optimized syscalls
        let host_path = self.root.join(path.strip_prefix("/").unwrap_or(path));
        let resolved = std::fs::canonicalize(&host_path)
            .map_err(|e| FsError::NotFound { 
                path: path.into(), 
                operation: "canonicalize" 
            })?;
        
        // Verify containment (security check)
        if !resolved.starts_with(&self.root) {
            return Err(FsError::AccessDenied {
                path: path.into(),
                reason: "path escapes root".into(),
            });
        }
        
        // Convert back to virtual path
        Ok(PathBuf::from("/").join(resolved.strip_prefix(&self.root).unwrap()))
    }
}
}

Other Optimization Opportunities

MethodDefaultOptimization Opportunity
canonicalize()O(n) per componentSQL CTE, OS delegation
create_dir_all()Recursive create_dir()Single SQL INSERT with path hierarchy
remove_dir_all()Recursive traversalSQL DELETE with LIKE pattern
copy()read + writeDatabase-level copy, reflink

General Pattern:

#![allow(unused)]
fn main() {
// Override any trait method with optimized implementation
impl FsDir for SqliteBackend {
    fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
        // Instead of calling create_dir() for each level,
        // insert all parent paths in a single transaction
        self.conn.execute_batch(&format!(
            "INSERT OR IGNORE INTO nodes (path, type) VALUES {}",
            generate_ancestor_values(path)
        ))?;
        Ok(())
    }
}
}

When NOT to optimize:

  • MemoryBackend: In-memory operations are already fast; keep it simple
  • Low-volume operations: Optimize where it matters (hot paths)
  • Prototype phase: Get correctness first, optimize later

See ADR-032 for the full design rationale.

Testing Your Backend

Use the conformance test suite:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::MyBackend;
    use anyfs_backend::Fs;

    fn create_backend() -> MyBackend {
        MyBackend::new()
    }

    #[test]
    fn test_write_read() {
        let backend = create_backend();
        backend.write(std::path::Path::new("/test.txt"), b"hello").unwrap();
        let content = backend.read(std::path::Path::new("/test.txt")).unwrap();
        assert_eq!(content, b"hello");
    }

    #[test]
    fn test_create_dir() {
        let backend = create_backend();
        backend.create_dir(std::path::Path::new("/foo")).unwrap();
        assert!(backend.exists(std::path::Path::new("/foo")).unwrap());
    }

    // ... more tests
}
}

Note on VRootFsBackend

If you are implementing a backend that wraps a real host filesystem directory, consider using strict-path::VirtualPath and strict-path::VirtualRoot internally for path containment. This ensures paths cannot escape the designated root directory.

This is an implementation choice for filesystem-based backends, not a requirement of the Fs trait.


For Middleware Authors: Wrapping Streams

Middleware that needs to intercept streaming I/O must wrap the returned Box<dyn Read/Write>.

CountingWriter Example

#![allow(unused)]
fn main() {
use std::io::{self, Write};
use std::sync::{Arc, atomic::{AtomicU64, Ordering}};

pub struct CountingWriter<W: Write> {
    inner: W,
    bytes_written: Arc<AtomicU64>,
}

impl<W: Write> CountingWriter<W> {
    pub fn new(inner: W, counter: Arc<AtomicU64>) -> Self {
        Self { inner, bytes_written: counter }
    }
}

impl<W: Write + Send> Write for CountingWriter<W> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let n = self.inner.write(buf)?;
        self.bytes_written.fetch_add(n as u64, Ordering::Relaxed);
        Ok(n)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.inner.flush()
    }
}
}

Using in Quota Middleware

#![allow(unused)]
fn main() {
impl<B: Fs> Fs for Quota<B> {
    fn open_write(&self, path: &Path) -> Result<Box<dyn Write + Send>, FsError> {
        // Check if we're at quota before opening
        if self.usage.total_bytes >= self.limits.max_total_size {
            return Err(FsError::QuotaExceeded { ... });
        }

        let inner = self.inner.open_write(path)?;
        Ok(Box::new(CountingWriter::new(inner, self.usage.bytes_counter.clone())))
    }
}
}

Alternatives to Wrapping

MiddlewareAlternative to wrapping
PathFilterCheck path at open time, pass stream through
ReadOnlyBlock open_write entirely
RateLimitCount the open call, not stream bytes
TracingLog the open call, pass stream through
DryRunReturn std::io::sink() instead of real writer

Creating Custom Middleware

Custom middleware only requires anyfs-backend as a dependency - same as backends.

Dependency

[dependencies]
anyfs-backend = "0.1"

The Pattern (5 Minutes to Understand)

Middleware is just a struct that:

  1. Wraps another Fs
  2. Implements Fs itself
  3. Intercepts some methods, delegates others
#![allow(unused)]
fn main() {
//  ┌─────────────────────────────────────┐
//  │  Your Middleware                    │
//  │  ┌─────────────────────────────────┐│
//  │  │  Inner Backend (any Fs) ││
//  │  └─────────────────────────────────┘│
//  └─────────────────────────────────────┘
//
//  Request → Middleware (intercept/modify) → Inner Backend
//  Response ← Middleware (intercept/modify) ← Inner Backend
}

Simplest Possible Middleware: Operation Counter

This middleware counts how many operations are performed:

#![allow(unused)]
fn main() {
use anyfs_backend::{Fs, FsError, Metadata, DirEntry, Permissions, StatFs};
use std::sync::atomic::{AtomicU64, Ordering};
use std::path::{Path, PathBuf};

/// Counts all operations performed on the backend.
pub struct Counter<B> {
    inner: B,
    pub count: AtomicU64,
}

impl<B> Counter<B> {
    pub fn new(inner: B) -> Self {
        Self { inner, count: AtomicU64::new(0) }
    }

    pub fn operations(&self) -> u64 {
        self.count.load(Ordering::Relaxed)
    }
}

// Implement each trait the inner backend supports
impl<B: FsRead> FsRead for Counter<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        self.count.fetch_add(1, Ordering::Relaxed);  // Count it
        self.inner.read(path)                         // Delegate
    }

    fn exists(&self, path: &Path) -> Result<bool, FsError> {
        self.count.fetch_add(1, Ordering::Relaxed);
        self.inner.exists(path)
    }

    // ... repeat for all FsRead methods
}

impl<B: FsWrite> FsWrite for Counter<B> {
    fn write(&self, path: &Path, data: &[u8]) -> Result<(), FsError> {
        self.count.fetch_add(1, Ordering::Relaxed);  // Count it
        self.inner.write(path, data)                  // Delegate
    }

    // ... repeat for all FsWrite methods
}

impl<B: FsDir> FsDir for Counter<B> {
    // ... implement FsDir methods
}

// Counter<B> now implements Fs when B: Fs (blanket impl)
}

Usage:

#![allow(unused)]
fn main() {
let backend = Counter::new(MemoryBackend::new());
backend.write(std::path::Path::new("/file.txt"), b"hello")?;
backend.read(std::path::Path::new("/file.txt"))?;
backend.read(std::path::Path::new("/file.txt"))?;

println!("Operations: {}", backend.operations());  // 3
}

That’s it. That’s the entire pattern.

Adding a Layer (for .layer() syntax)

To enable the fluent .layer() syntax, add a Layer struct. The .layer() method comes from the LayerExt trait which has a blanket impl for all Fs types:

#![allow(unused)]
fn main() {
use anyfs_backend::{Layer, LayerExt};  // LayerExt provides .layer() method

pub struct CounterLayer;

impl<B: Fs> Layer<B> for CounterLayer {
    type Backend = Counter<B>;

    fn layer(self, backend: B) -> Counter<B> {
        Counter::new(backend)
    }
}
}

Usage with .layer():

#![allow(unused)]
fn main() {
// LayerExt is re-exported from anyfs crate
use anyfs::LayerExt;

let backend = MemoryBackend::new()
    .layer(CounterLayer);
}

Real Example: ReadOnly Middleware

A practical middleware that blocks all write operations:

#![allow(unused)]
fn main() {
pub struct ReadOnly<B> {
    inner: B,
}

impl<B> ReadOnly<B> {
    pub fn new(inner: B) -> Self {
        Self { inner }
    }
}

// FsRead: just delegate
impl<B: FsRead> FsRead for ReadOnly<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        self.inner.read(path)
    }

    fn exists(&self, path: &Path) -> Result<bool, FsError> {
        self.inner.exists(path)
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        self.inner.metadata(path)
    }

    // ... delegate all FsRead methods
}

// FsDir: delegate reads, block writes
impl<B: FsDir> FsDir for ReadOnly<B> {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        self.inner.read_dir(path)
    }

    fn create_dir(&self, _path: &Path) -> Result<(), FsError> {
        Err(FsError::ReadOnly { operation: "create_dir" })
    }

    fn create_dir_all(&self, _path: &Path) -> Result<(), FsError> {
        Err(FsError::ReadOnly { operation: "create_dir_all" })
    }

    fn remove_dir(&self, _path: &Path) -> Result<(), FsError> {
        Err(FsError::ReadOnly { operation: "remove_dir" })
    }

    fn remove_dir_all(&self, _path: &Path) -> Result<(), FsError> {
        Err(FsError::ReadOnly { operation: "remove_dir_all" })
    }
}

// FsWrite: block all operations
impl<B: FsWrite> FsWrite for ReadOnly<B> {
    fn write(&self, _path: &Path, _data: &[u8]) -> Result<(), FsError> {
        Err(FsError::ReadOnly { operation: "write" })
    }

    fn remove_file(&self, _path: &Path) -> Result<(), FsError> {
        Err(FsError::ReadOnly { operation: "remove_file" })
    }

    // ... block all FsWrite methods
}
}

Usage:

#![allow(unused)]
fn main() {
let backend = ReadOnly::new(MemoryBackend::new());

backend.read(std::path::Path::new("/file.txt"));       // OK (if file exists)
backend.write(std::path::Path::new("/file.txt"), b""); // Error: ReadOnly
}

Middleware Decision Table

What You WantInterceptDelegateExample
Count operationsAll methods (before)All methodsCounter
Block writesWrite methodsRead methodsReadOnly
Transform dataread/writeEverything elseEncryption
Check permissionsAll methods (before)All methodsPathFilter
Log operationsAll methods (before)All methodsTracing
Enforce limitsWrite methods (check size)Read methodsQuota

Macro for Boilerplate (Optional)

If you don’t want to manually delegate all 29 methods, you can use a macro:

#![allow(unused)]
fn main() {
macro_rules! delegate {
    ($self:ident, $method:ident, $($arg:ident),*) => {
        $self.inner.$method($($arg),*)
    };
}

impl<B: Fs> Fs for MyMiddleware<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        // Your logic here
        delegate!(self, read, path)
    }

    fn exists(&self, path: &Path) -> Result<bool, FsError> {
        delegate!(self, exists, path)
    }

    // ... etc
}
}

Or provide a delegate_all! macro in anyfs-backend that generates all the passthrough implementations.

Complete Example: Encryption Middleware

#![allow(unused)]
fn main() {
use anyfs_backend::{FsRead, FsWrite, FsDir, Layer, FsError, Metadata, DirEntry};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

/// Middleware that encrypts/decrypts file contents transparently.
pub struct Encrypted<B> {
    inner: B,
    key: [u8; 32],
}

impl<B> Encrypted<B> {
    pub fn new(inner: B, key: [u8; 32]) -> Self {
        Self { inner, key }
    }

    fn encrypt(&self, data: &[u8]) -> Vec<u8> {
        // Your encryption logic here
        data.iter().map(|b| b ^ self.key[0]).collect()
    }

    fn decrypt(&self, data: &[u8]) -> Vec<u8> {
        // Your decryption logic here (symmetric for XOR)
        self.encrypt(data)
    }
}

// FsRead: decrypt on read
impl<B: FsRead> FsRead for Encrypted<B> {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let encrypted = self.inner.read(path)?;
        Ok(self.decrypt(&encrypted))
    }

    fn exists(&self, path: &Path) -> Result<bool, FsError> {
        self.inner.exists(path)
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        self.inner.metadata(path)
    }

    // ... delegate other FsRead methods
}

// FsWrite: encrypt on write
impl<B: FsWrite> FsWrite for Encrypted<B> {
    fn write(&self, path: &Path, data: &[u8]) -> Result<(), FsError> {
        let encrypted = self.encrypt(data);
        self.inner.write(path, &encrypted)
    }

    // ... delegate/encrypt other FsWrite methods
}

// FsDir: just delegate (directories don't need encryption)
impl<B: FsDir> FsDir for Encrypted<B> {
    fn read_dir(&self, path: &Path) -> Result<ReadDirIter, FsError> {
        self.inner.read_dir(path)
    }

    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        self.inner.create_dir(path)
    }

    // ... delegate other FsDir methods
}

// Encrypted<B> now implements Fs when B: Fs (blanket impl)

/// Layer for creating Encrypted middleware.
pub struct EncryptedLayer {
    key: [u8; 32],
}

impl EncryptedLayer {
    pub fn new(key: [u8; 32]) -> Self {
        Self { key }
    }
}

impl<B: Fs> Layer<B> for EncryptedLayer {
    type Backend = Encrypted<B>;

    fn layer(self, backend: B) -> Self::Backend {
        Encrypted::new(backend, self.key)
    }
}
}

Usage

#![allow(unused)]
fn main() {
use anyfs::MemoryBackend;
use my_middleware::{EncryptedLayer, Encrypted};

// Direct construction
let fs = Encrypted::new(MemoryBackend::new(), key);

// Or via Layer trait
let fs = MemoryBackend::new()
    .layer(EncryptedLayer::new(key));
}

Middleware Checklist

  • Depends only on anyfs-backend
  • Implements the same traits as the inner backend (FsRead, FsWrite, FsDir, etc.)
  • Implements Layer<B> for MyMiddlewareLayer
  • Delegates unmodified operations to inner backend
  • Handles streaming I/O appropriately (wrap, pass-through, or block)
  • Documents which operations are intercepted vs delegated

Backend Checklist

  • Depends only on anyfs-backend
  • Implements core traits: FsRead, FsWrite, FsDir (= Fs)
  • Optional: Implements FsLink, FsPermissions, FsSync, FsStats (= FsFull)
  • Optional: Implements FsInode for FUSE support (= FsFuse)
  • Optional: Implements FsHandles, FsLock, FsXattr for POSIX (= FsPosix)
  • Accepts &Path for all paths
  • Returns correct FsError variants
  • Passes conformance tests for implemented traits
  • No panics (see below)
  • Thread-safe (see below)
  • Documents performance characteristics

Critical Implementation Guidelines

These guidelines are derived from issues found in similar projects (vfs, agentfs). All implementations MUST follow these.

1. No Panic Policy

NEVER use .unwrap() or .expect() in library code.

#![allow(unused)]
fn main() {
// BAD - will panic on missing file
fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
    let entry = self.entries.get(path.as_ref()).unwrap();  // PANIC!
    Ok(entry.content.clone())
}

// GOOD - returns error
fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
    let path = path.as_ref();
    let entry = self.entries.get(path)
        .ok_or_else(|| FsError::NotFound { path: path.to_path_buf() })?;
    Ok(entry.content.clone())
}
}

Edge cases that must NOT panic:

  • File doesn’t exist
  • Directory doesn’t exist
  • Path is empty string
  • Path is invalid UTF-8 (if using OsStr)
  • Parent directory missing
  • Trying to read a directory as a file
  • Trying to list a file as a directory
  • Concurrent access conflicts

2. Thread Safety (Required)

All trait methods use &self, not &mut self. This means backends MUST use interior mutability for thread-safe concurrent access.

Why &self?

  • Enables concurrent access patterns (multiple readers, concurrent operations)
  • Matches real filesystem semantics (concurrent access is normal)
  • More flexible API (can share references without exclusive ownership)

Backend implementer responsibility:

  • Use RwLock, Mutex, or similar for internal state
  • Ensure operations are atomic (a single write() call shouldn’t produce partial results)
  • Handle lock poisoning gracefully

What the synchronization guarantees:

  • Memory safety (no data corruption)
  • Atomic operations (writes don’t interleave)

What it does NOT guarantee:

  • Order of concurrent writes to the same path (last write wins - standard FS behavior)
#![allow(unused)]
fn main() {
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use std::path::PathBuf;

pub struct MemoryBackend {
    entries: Arc<RwLock<HashMap<PathBuf, Entry>>>,
}

impl FsRead for MemoryBackend {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        let entries = self.entries.read()
            .map_err(|_| FsError::Backend("lock poisoned".into()))?;
        // ...
    }
}

impl FsWrite for MemoryBackend {
    fn write(&self, path: &Path, data: &[u8]) -> Result<(), FsError> {
        let mut entries = self.entries.write()
            .map_err(|_| FsError::Backend("lock poisoned".into()))?;
        // ...
    }
}
}

Common race conditions to avoid:

  • create_dir_all called concurrently for same path
  • read during write to same file
  • read_dir while directory is being modified
  • rename with concurrent access to source or destination

3. Path Resolution - NOT Your Job

Backends do NOT handle path resolution. FileStorage handles:

  • Resolving .. and . components
  • Following symlinks for non-SelfResolving backends that implement FsLink
  • Normalizing paths (///, trailing slashes, etc.)
  • Walking the virtual directory structure

Your backend receives already-resolved, clean paths. Just store and retrieve bytes at those paths.

#![allow(unused)]
fn main() {
impl FsRead for MyBackend {
    fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
        // Path is already resolved - just use it directly
        let path = path.as_ref();
        self.storage.get(path).ok_or_else(|| FsError::NotFound { path: path.to_path_buf() })
    }
}
}

Exception: If your backend wraps a real filesystem (like VRootFsBackend), implement SelfResolving to tell FileStorage to skip resolution - the OS handles it.

#![allow(unused)]
fn main() {
impl SelfResolving for VRootFsBackend {}
}

4. Error Messages

Include context in errors for debugging:

#![allow(unused)]
fn main() {
// BAD - no context
Err(FsError::NotFound)

// GOOD - includes path
Err(FsError::NotFound { path: path.to_path_buf() })

// BETTER - includes operation context
Err(FsError::Io {
    path: path.to_path_buf(),
    operation: "read",
    source: io_error,
})
}

5. Drop Implementation

Ensure cleanup happens correctly:

#![allow(unused)]
fn main() {
impl Drop for SqliteBackend {
    fn drop(&mut self) {
        // Flush any pending writes
        if let Err(e) = self.sync() {
            eprintln!("Warning: failed to sync on drop: {}", e);
        }
    }
}
}

6. Performance Documentation

Document the complexity of operations:

#![allow(unused)]
fn main() {
/// Memory-based virtual filesystem backend.
///
/// # Performance Characteristics
///
/// | Operation | Complexity | Notes |
/// |-----------|------------|-------|
/// | `read` | O(1) | HashMap lookup |
/// | `write` | O(n) | n = data size |
/// | `read_dir` | O(k) | k = entries in directory |
/// | `create_dir_all` | O(d) | d = path depth |
/// | `remove_dir_all` | O(n) | n = total descendants |
///
/// # Thread Safety
///
/// All operations are thread-safe. Uses `RwLock` internally.
/// Multiple concurrent reads are allowed.
/// Writes are exclusive.
pub struct MemoryBackend { ... }
}

Testing Requirements

Your backend MUST pass these test categories:

Basic Functionality

#![allow(unused)]
fn main() {
#[test]
fn test_read_write_roundtrip() { ... }

#[test]
fn test_create_dir_and_list() { ... }

#[test]
fn test_remove_file() { ... }
}

Edge Cases (No Panics)

#![allow(unused)]
fn main() {
#[test]
fn test_read_nonexistent_returns_error() {
    let backend = create_backend();
    assert!(matches!(
        backend.read(std::path::Path::new("/nonexistent")),
        Err(FsError::NotFound { .. })
    ));
}

#[test]
fn test_read_dir_on_file_returns_error() {
    let backend = create_backend();
    backend.write(std::path::Path::new("/file.txt"), b"data").unwrap();
    assert!(matches!(
        backend.read_dir(std::path::Path::new("/file.txt")),
        Err(FsError::NotADirectory { .. })
    ));
}

#[test]
fn test_empty_path_returns_error() {
    let backend = create_backend();
    assert!(backend.read(std::path::Path::new("")).is_err());
}
}

Thread Safety

#![allow(unused)]
fn main() {
#[test]
fn test_concurrent_reads() {
    let backend = Arc::new(create_backend_with_data());
    let handles: Vec<_> = (0..10).map(|_| {
        let backend = backend.clone();
        std::thread::spawn(move || {
            for _ in 0..100 {
                backend.read(std::path::Path::new("/test.txt")).unwrap();
            }
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }
}

#[test]
fn test_concurrent_create_dir_all() {
    let backend = Arc::new(RwLock::new(create_backend()));
    let handles: Vec<_> = (0..10).map(|_| {
        let backend = backend.clone();
        std::thread::spawn(move || {
            let mut backend = backend.write().unwrap();
            // Should not panic or corrupt state
            let _ = backend.create_dir_all(std::path::Path::new("/a/b/c/d"));
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }
}
}

Path Normalization

Note: These tests apply to FileStorage integration tests, NOT direct backend tests. Backends receive already-resolved paths from FileStorage. The tests below verify that FileStorage correctly normalizes paths before passing them to backends.

#![allow(unused)]
fn main() {
#[test]
fn test_filestorage_path_normalization() {
    // Use FileStorage, not raw backend
    let fs = FileStorage::new(create_backend());
    fs.create_dir_all("/foo/bar").unwrap();
    fs.write("/foo/bar/test.txt", b"data").unwrap();

    // FileStorage resolves these before calling backend
    assert_eq!(fs.read("/foo/bar/test.txt").unwrap(), b"data");
    assert_eq!(fs.read("/foo/bar/../bar/test.txt").unwrap(), b"data");
    assert_eq!(fs.read("/foo/./bar/test.txt").unwrap(), b"data");
}

// Direct backend calls should use clean paths only
#[test]
fn test_backend_with_clean_paths() {
    let backend = create_backend();
    backend.create_dir_all(std::path::Path::new("/foo/bar")).unwrap();
    backend.write(std::path::Path::new("/foo/bar/test.txt"), b"data").unwrap();

    // Backends receive clean, resolved paths
    assert_eq!(backend.read(std::path::Path::new("/foo/bar/test.txt")).unwrap(), b"data");
}
}

MemoryBackend Snapshot & Restore

MemoryBackend supports cloning its entire state (snapshot) and serializing to bytes for persistence.

Core Concept

Snapshot = Clone the storage. That’s it.

#![allow(unused)]
fn main() {
// MemoryBackend implements Clone (custom impl, not derive)
pub struct MemoryBackend { ... }

impl Clone for MemoryBackend {
    fn clone(&self) -> Self {
        // Deep copy of Arc<RwLock<...>> contents
        // ...
    }
}

// Snapshot is just .clone()
let snapshot = fs.clone();

// Restore is just assignment
fs = snapshot;
}

API

#![allow(unused)]
fn main() {
impl MemoryBackend {
    /// Clone the entire filesystem state.
    /// This is a DEEP COPY - modifications to the clone don't affect the original.
    /// Implemented via custom Clone (not #[derive(Clone)]) to ensure deep copy
    /// of Arc<RwLock<...>> contents.
    pub fn clone(&self) -> Self { ... }

    /// Serialize to bytes for persistence/transfer.
    pub fn to_bytes(&self) -> Result<Vec<u8>, FsError>;

    /// Deserialize from bytes.
    pub fn from_bytes(data: &[u8]) -> Result<Self, FsError>;

    /// Save to file.
    pub fn save_to(&self, path: impl AsRef<Path>) -> Result<(), FsError>;

    /// Load from file.
    pub fn load_from(path: impl AsRef<Path>) -> Result<Self, FsError>;
}
}

Usage

#![allow(unused)]
fn main() {
let fs = MemoryBackend::new();
fs.write(std::path::Path::new("/data.txt"), b"important")?;

// Snapshot = clone
let checkpoint = fs.clone();

// Do risky work...
fs.write(std::path::Path::new("/data.txt"), b"corrupted")?;

// Rollback = replace with clone
fs = checkpoint;
assert_eq!(fs.read(std::path::Path::new("/data.txt"))?, b"important");
}

Persistence

#![allow(unused)]
fn main() {
// Save to disk
fs.save_to("state.bin")?;

// Load from disk
let fs = MemoryBackend::load_from("state.bin")?;
}

SqliteBackend

SQLite already has persistence - the database file IS the snapshot. For explicit snapshots:

#![allow(unused)]
fn main() {
impl SqliteBackend {
    /// Create an in-memory copy of the database.
    pub fn clone_to_memory(&self) -> Result<Self, FsError>;

    /// Backup to another file.
    pub fn backup_to(&self, path: impl AsRef<Path>) -> Result<(), FsError>;
}
}