Skip to content

Latest commit

 

History

History
224 lines (168 loc) · 10.7 KB

File metadata and controls

224 lines (168 loc) · 10.7 KB

CryFS Architecture Guide

This document helps AI coding agents understand the CryFS codebase structure, patterns, and conventions.

Project Overview

CryFS is an encrypted filesystem for cloud storage (Dropbox, iCloud, OneDrive). It encrypts files while hiding file sizes, directory structure, and metadata.

  • Rust workspace with several crates
  • Targets newest Rust edition and version

Version Control

This project uses Jujutsu (jj) for version control, colocated with git. Use jj commands instead of git. See the jujutsu skill for comprehensive documentation.

Key concepts: No staging area (working copy IS a commit), bookmarks instead of branches, automatic rebasing.

Essential commands: jj st, jj diff, jj log, jj commit -m "msg", jj squash, jj git fetch, jj git push

Branch naming: main, feature/{name}, release/{version}

Architecture

The architecture is layered. Dependencies flow downward (higher layers depend on lower layers):

┌─────────────────────────────────────────────────────────────┐
│  APPLICATION LAYER                                          │
│  cryfs-cli                                                  │
│  (CLI binary + mount orchestration, foreground & daemon)    │
│       │                                                     │
│       └──► cli-utils                                        │
│            (shared CLI code, blockstore setup)              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  FILESYSTEM LAYER                                           │
│  cryfs-filesystem (CryDevice - implements FUSE operations) │
│       │                                                    │
│       └──► rustfs (FUSE abstraction, Device trait)         │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  STORAGE LAYER (bottom-up within this layer)               │
│                                                             │
│  fsblobstore  (filesystem semantics: file/dir/symlink)     │
│       │                                                     │
│       ▼                                                     │
│  blobstore    (variable-size data as trees of blocks)      │
│       │                                                     │
│       ▼                                                     │
│  blockstore   (fixed-size encrypted blocks)                │
└─────────────────────────────────────────────────────────────┘

Cross-cutting: crypto, utils, cryfs-version, concurrent-store, cryfs-config

Crate Directory

Storage Layer

Crate Purpose Key Types
blockstore Fixed-size encrypted blocks BlockId, BlockStore* traits
blobstore Variable-size data on blocks Blob, BlobStore, DataTree
fsblobstore Filesystem blob semantics FsBlobStore, ConcurrentFsBlobStore

Filesystem Layer

Crate Purpose Key Types
rustfs FUSE abstraction layer Device trait, fuser backend
cryfs-filesystem FUSE filesystem implementation CryDevice, CryFile, CryDir, CryNode

Application Layer

Crate Purpose Key Types
cryfs-cli Main CLI binary (cryfs) and mount orchestration argument parsing, console interaction, MountArgs, mount_filesystem, background_main (daemon side)
cli-utils Shared CLI utilities blockstore stack setup, password prompts, Application/ConstructibleApplication, run/run_with

Cross-cutting

Crate Purpose Key Types
crypto Cryptographic primitives Cipher, KDF, Hash
cryfs-config Configuration management CryConfig, LocalStateDir
utils Common utilities AsyncDrop, Data, temp files
concurrent-store Thread-safe caching ConcurrentStore
cryfs-version Version macros git tag verification

Testing/Development

Crate Purpose
check Filesystem integrity checker (cryfs-check binary)
tempproject Test utilities
e2e-perf-tests Performance benchmarks

BlockStore Stack

The blockstore uses a decorator pattern. Layers wrap each other (innermost to outermost):

OnDiskBlockStore (disk I/O)
      ↓
EncryptedBlockStore (encryption via crypto crate)
      ↓
IntegrityBlockStore (integrity verification, versioning)
      ↓
LockingBlockStore (per-block concurrency control)

See crates/cli-utils/src/blockstore_setup.rs for the setup code.

Coding Conventions

  • #![forbid(unsafe_code)] in most crates
  • All I/O is async via tokio runtime
  • Trait-based abstraction (BlockStoreReader/Writer/Deleter, Blob, Device)
  • Async traits use native Rust async support, not the async-trait crate: declare methods as fn f(&self) -> impl Future<Output = T> + Send; and implement them with async fn. Traits that must be dyn-compatible get a separate Dyn* trait with boxed futures (see DynLLBlockStore)
  • Binary serialization with binrw and binary-layout

Code Quality Principles

  • All code should have tests - write tests for new functionality
  • Clear architectural patterns with low coupling between components
  • Use invariants to reason about correctness
  • Use the type system to enforce correctness and invariants when possible
  • Prefer compile-time guarantees over runtime checks

Error Handling Patterns

  • Library crates: Define detailed error types with thiserror, NOT anyhow
  • CLI/application crates: May use anyhow for error propagation with .context()
  • Calling code responsibility: When calling library functions, check for errors and wrap/map them to your own error types where appropriate
  • Use panics (e.g. unwrap(), expect()) only for unrecoverable errors, e.g. invariant violatons that should not be possible to happen.
  • Note: Current codebase doesn't fully follow this yet, but new code should

AsyncDrop Pattern

Types needing async cleanup use AsyncDropGuard<T>. See the async-drop skill for comprehensive documentation.

Essential rules:

  • Every AsyncDropGuard<T> must have async_drop() called before drop (panics otherwise)
  • async_drop() consumes the guard: use-after-drop and double-drop are compile errors
  • Factory methods return AsyncDropGuard<Self>, never plain Self
  • Types with guard members must implement AsyncDrop to delegate: async fn async_drop_impl(self), destructure self listing every field (no ..), drop the guard members. Plain async fn, no attribute macros
  • Use with_async_drop! macro when possible; otherwise call async_drop() on all exit paths
  • Panics are exceptions - ok to skip async_drop() on panic paths

Implementation: crates/utils/src/async_drop/

Type-Driven Invariants

  • Use newtypes to enforce constraints (e.g., BlockId wraps fixed-size array)
  • Use NonZero types for IDs that can't be zero
  • Use enums to encode valid states (e.g., MaybeClientId::ClientId | BlockWasDeleted)
  • Prefer compile-time guarantees over runtime checks

Feature Flags

  • Use #[cfg(any(test, feature = "testutils"))] for test-only code
  • Keep test utilities in optional testutils feature
  • Re-export test utilities conditionally in lib.rs

Module Organization

  • Keep internal modules private
  • Re-export public types at crate root
  • Use conditional compilation for test utilities
  • Don't expose internal module structure

Testing

  • Unit tests: #[cfg(test)] modules next to the code being tested
  • Integration tests: crates/{crate}/tests/
  • Benchmarks: crates/{crate}/benches/ (criterion)
  • Frameworks: #[tokio::test], rstest, mockall, assert_cmd
  • Macro-generated test suites for testing multiple implementations

Test-Driven Development

Use test-driven development (TDD) where it makes sense:

  1. Write the test first - Before implementing a feature or modification, write a test that specifies the expected behavior
  2. Verify the test fails - Run the test and confirm it fails (this validates the test is actually testing something)
  3. Write the implementation - Implement the feature or modification to make the test pass
  4. Verify the test passes - Run the test and confirm it now passes

If you need to modify the test after writing the implementation, you must verify that the updated test still fails against the code before your implementation. This ensures the test is valid and not just passing due to a testing error.

Build Commands

cargo build --release          # Build release binary
cargo test                     # Run all tests
cargo test -p cryfs-cli        # Test specific crate
cargo fmt                      # Format code
cargo doc                      # Generate docs

Key File Locations

  • Entry point: crates/cryfs-cli/src/bin/cryfs.rs
  • CLI args: crates/cryfs-cli/src/args/
  • Mount orchestration (foreground and daemon mode): crates/cryfs-cli/src/runner/
  • Core filesystem: crates/cryfs-filesystem/src/
  • Block encryption: crates/blockstore/src/low_level/implementations/encrypted/
  • Integrity: crates/blockstore/src/low_level/implementations/integrity/
  • On-disk storage: crates/blockstore/src/low_level/implementations/ondisk/
  • Ciphers: crates/crypto/src/symmetric/
  • FUSE backend: crates/rustfs/src/
  • Configuration: crates/cryfs-config/src/
  • CI: .github/workflows/ci.yml

Common Development Tasks

  • Adding a new cipher: Implement in crates/crypto/src/symmetric/, register in config ciphers
  • Adding CLI option: Modify crates/cryfs-cli/src/args/, thread it through MountArgs in crates/cryfs-cli/src/runner/
  • Adding filesystem operation: Implement in crates/cryfs-filesystem/, wire through rustfs Device trait
  • Adding tests: Follow existing patterns, use macro fixtures for multiple implementations