This document helps AI coding agents understand the CryFS codebase structure, patterns, and conventions.
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
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}
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 | 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 |
| Crate | Purpose | Key Types |
|---|---|---|
| rustfs | FUSE abstraction layer | Device trait, fuser backend |
| cryfs-filesystem | FUSE filesystem implementation | CryDevice, CryFile, CryDir, CryNode |
| 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 |
| 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 |
| Crate | Purpose |
|---|---|
| check | Filesystem integrity checker (cryfs-check binary) |
| tempproject | Test utilities |
| e2e-perf-tests | Performance benchmarks |
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.
#![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-traitcrate: declare methods asfn f(&self) -> impl Future<Output = T> + Send;and implement them withasync fn. Traits that must be dyn-compatible get a separateDyn*trait with boxed futures (seeDynLLBlockStore) - Binary serialization with
binrwandbinary-layout
- 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
- Library crates: Define detailed error types with
thiserror, NOTanyhow - CLI/application crates: May use
anyhowfor 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
Types needing async cleanup use AsyncDropGuard<T>. See the async-drop skill for comprehensive documentation.
Essential rules:
- Every
AsyncDropGuard<T>must haveasync_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 plainSelf - Types with guard members must implement
AsyncDropto delegate:async fn async_drop_impl(self), destructureselflisting every field (no..), drop the guard members. Plainasync fn, no attribute macros - Use
with_async_drop!macro when possible; otherwise callasync_drop()on all exit paths - Panics are exceptions - ok to skip
async_drop()on panic paths
Implementation: crates/utils/src/async_drop/
- Use newtypes to enforce constraints (e.g.,
BlockIdwraps fixed-size array) - Use
NonZerotypes 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
- Use
#[cfg(any(test, feature = "testutils"))]for test-only code - Keep test utilities in optional
testutilsfeature - Re-export test utilities conditionally in
lib.rs
- Keep internal modules private
- Re-export public types at crate root
- Use conditional compilation for test utilities
- Don't expose internal module structure
- 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
Use test-driven development (TDD) where it makes sense:
- Write the test first - Before implementing a feature or modification, write a test that specifies the expected behavior
- Verify the test fails - Run the test and confirm it fails (this validates the test is actually testing something)
- Write the implementation - Implement the feature or modification to make the test pass
- 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.
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- 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
- 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 throughMountArgsincrates/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