# CLAUDE.md - hcfs-client

Core sync engine library for HCFS. Handles encryption, signing, and three-tree synchronization.

## Build & Test

```bash
cargo build -p hcfs-client
cargo test -p hcfs-client
```

## Key Files

| File | Purpose |
|------|---------|
| `src/lib.rs` | Module exports |
| `src/drive.rs` | `Drive` struct - main entry point, encryption, key management |
| `src/sync.rs` | Three-tree sync algorithm, `SyncPlan`, conflict types |
| `src/client.rs` | `HcfsClient` - HTTP client for server communication |
| `src/auth.rs` | Mnemonic encryption/recovery |

## Architecture

```
Drive (entry point)
├── Cryptography (BIP-39, Ed25519, XChaCha20-Poly1305)
├── SyncState (local, remote, synced trees)
├── SyncPlan (computed actions)
└── HcfsClient (network operations)
```

## Drive Struct

```rust
pub struct Drive {
    root_path: PathBuf,
    config_directory: PathBuf,      // .hippius/
    client: Option<HcfsClient>,
    signing_key: Option<SigningKey>,    // Ed25519 (after unlock)
    encryption_key: Option<[u8; 32]>,   // XChaCha20 (after unlock)
    user_id: Option<String>,            // hex(public_key)
    // Progress callbacks...
}
```

## FileMetadata

```rust
pub struct FileMetadata {
    pub path_hash: [u8; 32],       // BLAKE3 of relative path (file identifier)
    pub salted_hash: [u8; 32],     // BLAKE3(user_id || plaintext) for content comparison
    pub size_bytes: u64,           // Plaintext file size (consistent across client/server)
    pub revision_seq: u64,         // Monotonic sequence for replay protection
    pub revision_id: [u8; 32],     // Random unique version identifier
    pub encryption_nonce: [u8; 24], // XChaCha20-Poly1305 nonce
}
```

**Important:** `size_bytes` always refers to **plaintext size** for consistency. Content comparison uses `salted_hash`, not size.

### Key Methods

```rust
impl Drive {
    pub fn new(path: &Path) -> Self;
    pub fn init(&mut self, password: &str, mnemonic: Option<&str>) -> Result<String>;
    pub fn unlock(&mut self, password: &str) -> Result<()>;
    pub fn stage(&self) -> SyncResult<SyncPlan>;
    pub async fn sync_with_resolver<F>(&mut self, mode: SyncMode, resolver: F) -> SyncResult<SyncOutcome>;

    // Internal
    fn encrypt_file(&self, plaintext_path: &Path) -> Result<(PathBuf, FileMetadata)>;
    fn decrypt_file(&self, ciphertext_path: &Path, output_path: &Path) -> Result<()>;
    pub fn scan_local_files(&self, state: &mut SyncState) -> SyncResult<()>;
    pub async fn fetch_remote_state(&self, state: &mut SyncState) -> SyncResult<()>;
}
```

## Three-Tree Sync Model

```rust
pub struct SyncState {
    pub local: FileTree,      // Scanned from disk
    pub remote: FileTree,     // Fetched from server
    pub synced: FileTree,     // Last known sync state
    pub path_index: HashMap<FileId, PathBuf>,  // path_hash → relative path
}

pub struct FileTree {
    pub version: u64,
    pub files: HashMap<FileId, FileMetadata>,
}
```

### SyncPlan::build()

Classification logic in `sync.rs`:

```rust
pub fn classify_file(
    local: Option<&FileMetadata>,
    remote: Option<&FileMetadata>,
    synced: Option<&FileMetadata>,
) -> SyncAction
```

| Local | Remote | Synced | Action |
|-------|--------|--------|--------|
| A | A | A | Unchanged |
| A | - | - | LocalCreate → upload |
| - | A | - | RemoteCreate → download |
| A' | A | A | LocalModify → upload |
| A | A' | A | RemoteModify → download |
| - | A | A | LocalDelete → remote delete |
| A | - | A | RemoteDelete → local delete |
| A' | A'' | A | **Conflict** (ModifyModify) |

## Cryptographic Operations

### Key Derivation

```rust
// In unlock()
let mnemonic = recover_mnemonic(&path, password)?;
let seed = mnemonic.to_seed("");  // Empty passphrase for determinism

// Signing key (Ed25519)
let signing_key = SigningKey::from_bytes(&seed[..32]);

// User ID = hex(public_key)
let user_id = hex::encode(signing_key.verifying_key().as_bytes());

// Encryption key (same 32 bytes for XChaCha20-Poly1305)
let encryption_key = seed[..32];
```

### File Encryption

```rust
// Streaming encryption in chunks (64KB)
const ENCRYPTION_CHUNK_SIZE: usize = 64 * 1024;

fn encrypt_file(&self, plaintext_path: &Path) -> Result<(PathBuf, FileMetadata)> {
    let cipher = XChaCha20Poly1305::new(&key);
    let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);

    // For each chunk:
    // ciphertext_chunk = cipher.encrypt(&nonce, chunk)?
    // Write: [nonce (24 bytes)][chunk1][chunk2]...
}
```

### Path Hashing

```rust
fn compute_path_hash(relative_path: &Path) -> FileId {
    blake3::hash(relative_path.to_string_lossy().as_bytes()).into()
}
```

### Salted Hash (Dedup)

```rust
fn compute_salted_hash(user_id: &str, plaintext: &[u8]) -> [u8; 32] {
    let mut hasher = blake3::Hasher::new();
    hasher.update(user_id.as_bytes());
    hasher.update(plaintext);
    hasher.finalize().into()
}
```

## Conflict Resolution

```rust
pub enum SyncConflictResolution {
    KeepLocal,      // Upload local, overwrite remote
    AcceptRemote,   // Download remote, overwrite local
    KeepBoth,       // Save local as .conflict, download remote
    Skip,           // Handle later
}

pub enum SyncConflict {
    Upload(SyncUploadConflict),   // Server rejected (stale base)
    Plan(SyncPlanConflict),       // Three-tree detected conflict
}
```

## HcfsClient

```rust
pub struct HcfsClient {
    client: reqwest::Client,
    config: HcfsClientConfig,
}

impl HcfsClient {
    pub async fn upload(&self, manifest: &Manifest, ciphertext_path: &Path, progress: Option<...>) -> Result<UploadResult>;
    pub async fn download(&self, user_id: &str, file_id: &str, output: &Path, progress: Option<...>) -> Result<DownloadResult>;
    pub async fn delete(&self, user_id: &str, file_id: &str) -> Result<DeleteResult>;
    pub async fn get_state(&self, user_id: &str, offset: u32, limit: u32) -> Result<GetStateResult>;
}
```

### Server Response Headers

During downloads, the server returns:
- `X-Size-Bytes`: Plaintext file size (for metadata)
- `Content-Length`: Ciphertext size (for HTTP progress)
- `X-Revision-Id`: Hex-encoded version identifier
- `X-Revision-Seq`: Monotonic sequence number

## Progress Callbacks

```rust
pub type SyncProgressFn = Arc<dyn Fn(u64, u64, Option<&str>) + Send + Sync>;
// (bytes_completed, total_bytes, file_path)

pub struct SyncProgress {
    pub on_upload_progress: Option<SyncProgressFn>,
    pub on_download_progress: Option<SyncProgressFn>,
    pub on_encrypt_progress: Option<SyncProgressFn>,
    pub on_decrypt_progress: Option<SyncProgressFn>,
    pub on_scan_progress: Option<ScanProgressFn>,
    pub on_fetch_state_progress: Option<FetchProgressFn>,
}
```

## File Layout

```
drive/
├── .hippius/
│   ├── enc_mnemonic.json    # AES-256-GCM encrypted
│   ├── sync_state.json      # SyncState serialized
│   ├── sync_state.json.bak  # Backup before write
│   ├── config.json          # HcfsClientConfig
│   └── temp/                # Temp files during operations
└── [user files]
```

## Common Tasks

### Adding a new sync action

1. Add variant to `SyncAction` enum in `sync.rs`
2. Update `classify_file()` logic
3. Handle in `Drive::execute_sync_plan()`

### Modifying encryption

1. Update constants: `ENCRYPTION_CHUNK_SIZE`
2. Modify `encrypt_file()` / `decrypt_file()` in `drive.rs`
3. Ensure backward compatibility with existing files

### Adding progress callback

1. Add field to `SyncProgress` struct
2. Add corresponding field to `Drive`
3. Call callback at appropriate points in operation
