docs: add comprehensive doc comments to all Rust source files

Document every public function, struct, field, constant, and non-trivial
private function across idfoto-core and idfoto-cli. Module-level docs
explain each module's role in the architecture. Comments explain the "why"
(crypto choices, algorithm design, data model rationale) not just the "what".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-12 09:01:48 -04:00
parent 0d374f3faf
commit 847051216d
7 changed files with 823 additions and 38 deletions

View File

@@ -1,8 +1,48 @@
//! Vault data model: entries, manifest entries, and the manifest index.
//!
//! The vault stores credentials in two tiers:
//!
//! 1. **Individual entries** (`entries/<id>.enc`): each file contains a single
//! [`Entry`] encrypted with the master key. Only decrypted when the user
//! needs to read or edit a specific credential.
//!
//! 2. **Manifest** (`manifest.enc`): a single encrypted file containing a
//! [`Manifest`] -- a map from entry IDs to [`ManifestEntry`] summaries.
//! This lets the CLI list and search entries by decrypting only one file,
//! rather than decrypting every entry in the vault.
//!
//! ## Entry IDs
//!
//! Entry IDs are random 8-character lowercase hex strings (4 bytes of entropy,
//! ~4 billion possible values). This is sufficient for family-scale vaults while
//! keeping filenames short and filesystem-friendly.
//!
//! ## Serialization strategy
//!
//! All structs derive `Serialize`/`Deserialize` for JSON encoding. Optional fields
//! use `#[serde(skip_serializing_if = "Option::is_none")]` to keep the JSON compact
//! -- omitting null fields reduces ciphertext size and avoids leaking structural
//! information about which optional fields a credential uses.
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// A single password entry (stored encrypted in entries/<id>.enc).
/// A full credential entry stored encrypted in `entries/<id>.enc`.
///
/// Contains all sensitive data for a single credential. Each entry is encrypted
/// independently, so accessing one entry does not require decrypting others.
///
/// ## Fields
///
/// - `name`: human-readable label (e.g., "GitHub", "Work Email"). Required.
/// - `url`: the login URL. Optional; used for autofill matching in the browser extension.
/// - `username`: the account username or email. Optional.
/// - `password`: the credential password. Required (this is the core secret).
/// - `notes`: free-form text (e.g., security questions, recovery codes). Optional.
/// - `totp_secret`: base32-encoded TOTP secret for 2FA. Optional.
/// - `created_at`: ISO 8601 timestamp (or Unix seconds) when the entry was created.
/// - `updated_at`: ISO 8601 timestamp (or Unix seconds) of the last modification.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entry {
pub name: String,
@@ -19,25 +59,46 @@ pub struct Entry {
pub updated_at: String,
}
/// Summary info about an entry (stored in the manifest).
/// Summary metadata for a single entry, stored in the manifest.
///
/// This is a lightweight projection of [`Entry`] that contains only the
/// non-sensitive fields needed for listing and searching. The password,
/// notes, and TOTP secret are intentionally excluded so that listing
/// entries requires decrypting only the manifest, not every individual entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestEntry {
/// Human-readable label for display and search matching.
pub name: String,
/// Login URL for search matching and browser extension autofill.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Account username for display in entry listings.
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
/// Timestamp of last modification, used for sorting and display.
pub updated_at: String,
}
/// The vault manifest — maps entry IDs to their metadata.
/// The vault manifest -- an encrypted index mapping entry IDs to their metadata.
///
/// The manifest serves two purposes:
///
/// 1. **Efficient listing**: decrypting the single manifest file is enough to show
/// all entry names, URLs, and usernames without touching individual entry files.
/// 2. **Search**: the [`search`](Manifest::search) method performs case-insensitive
/// substring matching against entry names and URLs.
///
/// The `version` field allows future schema migrations if the manifest format evolves.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
/// Map from entry ID (8-char hex string) to entry metadata.
pub entries: HashMap<String, ManifestEntry>,
/// Schema version. Currently always `1`.
pub version: u32,
}
impl Manifest {
/// Create a new empty manifest with version 1.
pub fn new() -> Self {
Manifest {
entries: HashMap::new(),
@@ -45,14 +106,23 @@ impl Manifest {
}
}
/// Insert or update an entry in the manifest.
///
/// If an entry with the same ID already exists, it is overwritten.
/// This is used both for `add` (new entry) and `edit` (update existing).
pub fn add_entry(&mut self, id: String, entry: ManifestEntry) {
self.entries.insert(id, entry);
}
/// Remove an entry from the manifest by ID, returning its metadata if it existed.
pub fn remove_entry(&mut self, id: &str) -> Option<ManifestEntry> {
self.entries.remove(id)
}
/// Search entries by case-insensitive substring match against name and URL.
///
/// Returns a vector of `(id, entry)` pairs for all matching entries. An entry
/// matches if the query appears in its name or URL (case-insensitive).
pub fn search(&self, query: &str) -> Vec<(&String, &ManifestEntry)> {
let q = query.to_lowercase();
self.entries
@@ -75,6 +145,10 @@ impl Default for Manifest {
}
/// Generate a random 8-character hex string to use as an entry ID.
///
/// Uses 4 random bytes (32 bits of entropy), producing IDs like `"a1b2c3d4"`.
/// This gives ~4 billion possible values, which is more than sufficient for
/// a family-scale vault (typically < 1000 entries).
pub fn generate_entry_id() -> String {
let mut rng = rand::thread_rng();
let bytes: [u8; 4] = rng.gen();