# Architecture

## Layered Design

```
commands/     ← orchestration only — never imports tar, zstd, rage, sha2, reqwest directly
├── store/       ← local state: config.toml, manifest.toml, .jiignore, XDG paths
├── crypto/      ← Cipher trait + age/pgp backends
├── archive/     ← .ji file read/write (wraps tar+zstd+crypto+hmac internally)
└── remote/      ← Remote trait + webdav/ssh backends
```

### Dependency rules

- `commands/` references `store/`, `crypto/`, `archive/`, `remote/` — orchestrates, never implements
- `store/`, `crypto/`, `archive/`, `remote/` are independent of each other
- `store/path.rs` has no internal dependencies
- `archive/` is the only module that imports `tar`, `zstd`, and `hmac` crates
- Feature flags: `pgp` (sequoia-openpgp), `ssh` (russh)

## .ji File Format

```
┌──────────────────────────────┐
│ magic: 0xE6 0xAC 0x88        │  ← "笈" in UTF-8 (3 bytes)
│ version: u8 (1)               │
│ cipher: u8 (0=age, 1=pgp)    │
│ reserved: u16                 │
│ index_len: u32 LE             │
├──────────────────────────────┤
│ plaintext index               │  ← inspectable without decryption
│   entry_count: u32 LE         │
│   entries: [{                 │
│     name_len: u16 LE          │
│     name: UTF-8 bytes         │
│     size: u64 LE              │
│   }]                          │
│   total_size: u64 LE          │
│   HMAC-SHA256 (32 bytes)      │  ← signed with application key
├──────────────────────────────┤
│ encrypted payload             │
│   encrypt( zstd( tar(...) )) │
└──────────────────────────────┘
```

### Header (11 bytes)

| Offset | Size | Field |
|---|---|---|
| 0 | 3 | Magic: `0xE6 0xAC 0x88` |
| 3 | 1 | Version (currently `1`) |
| 4 | 1 | Cipher type (`0`=age, `1`=pgp) |
| 5 | 2 | Reserved (must be `0x00 0x00`) |
| 7 | 4 | Index length in bytes (u32 LE) |

### Plaintext index

Precedes the encrypted payload. Protected by HMAC-SHA256 with an application-level key. This enables `ji check` (fast mode) and `ji list` on a `.ji` file without decryption.

HMAC key is `b"ji-dotfiles-hmac-key-v1"` — provides integrity, not confidentiality.

Bounds enforced:
- `entry_count` ≤ 100,000
- `name_len` ≤ 4,096 bytes
- Path traversal patterns (`..`) rejected on extraction

### Encrypted payload

The payload is constructed in this order:

1. **tar** — archive containing `.ji_manifest.toml` + `files/<relative-path>` for each tracked file. POSIX metadata (mode, size) preserved.
2. **zstd** — compressed at level 3 (balance of speed and ratio)
3. **encrypt** — age (default) or PGP, using recipients from config.toml

## Encryption

### Cipher trait

```rust
pub trait Cipher {
    fn encrypt(data: &[u8], recipients: &[String]) -> Result<Vec<u8>>;
    fn decrypt(data: &[u8]) -> Result<Vec<u8>>;
    fn list_recipients(data: &[u8]) -> Result<Vec<String>>;
}
```

Two backends:

| Backend | Crate | Feature |
|---|---|---|
| age | `age` (with `ssh` feature) | Always enabled |
| PGP | `sequoia-openpgp` | `--features pgp` |

### Key discovery (age)

On decrypt, age searches for private keys in this order:

1. `~/.local/share/ji/ji.identity.age` (generated by `ji init`)
2. `~/.ssh/` — any file that isn't `.pub`, `known_hosts`, or `authorized_keys`
3. ssh-agent (detected via `SSH_AUTH_SOCK`)

### Key discovery (PGP)

On decrypt, PGP searches:

1. `~/.gnupg/pubring.gpg` / `pubring.kbx` / `secring.gpg`
2. `~/.gnupg/private-keys-v1.d/` (GnuPG 2.1+)

## Remote Transport

### Remote trait

```rust
pub trait Remote {
    fn push(&self, local: &Path, name: &str) -> Result<()>;
    fn pull(&self, name: &str, local: &Path) -> Result<()>;
    fn list(&self) -> Result<Vec<RemoteEntry>>;
    fn delete(&self, name: &str) -> Result<()>;
    fn test(&self) -> Result<()>;
}
```

Two backends:

| Backend | Crate | Auth | Feature |
|---|---|---|---|
| WebDAV | `reqwest` (blocking) | HTTP Basic Auth, password per session | Always enabled |
| SSH | System `ssh` CLI | SSH key + password | `--features ssh` |

Passwords are never stored on disk. Each push/pull prompts interactively via `rpassword`.

## Atomic Writes

All file writes use the same pattern:

```
write to <target>.ji_tmp
fsync
rename <target>.ji_tmp → <target>
```

This guarantees:
- Power loss or Ctrl+C at worst leaves orphaned `.ji_tmp` files
- The original file is never partially overwritten
- rename(2) is atomic on the same filesystem

Applies to: `ji pack`, `ji unpack`, `ji init`, `ji pull`, manifest writes.

## Config Directory Layout

```
~/.config/ji/
├── config.toml       # encryption settings + remote endpoints
├── manifest.toml     # tracked files: { path → checksum }
└── .jiignore         # exclusion rules (gitignore syntax)

~/.local/share/ji/
├── ji.identity.age        # private key (0600)
├── ji.identity.age.pub    # public key
└── cache/                 # file snapshots from last pack (0700)
    ├── .zshrc
    └── .gitconfig
```
