diff --git a/crates/chanora_bridge/src/lib.rs b/crates/chanora_bridge/src/lib.rs index 986380a..75235a5 100644 --- a/crates/chanora_bridge/src/lib.rs +++ b/crates/chanora_bridge/src/lib.rs @@ -98,6 +98,12 @@ pub enum BridgeError { Unmapped(String), } +impl BridgeError { + fn unmapped_ctx(ctx: impl std::fmt::Display, e: impl std::fmt::Display) -> Self { + BridgeError::Unmapped(format!("{ctx}: {e}")) + } +} + impl From for BridgeError { fn from(e: chanora_core::CoreError) -> Self { match e { diff --git a/crates/chanora_cache/src/lib.rs b/crates/chanora_cache/src/lib.rs index 68f79c9..4d1cbad 100644 --- a/crates/chanora_cache/src/lib.rs +++ b/crates/chanora_cache/src/lib.rs @@ -29,6 +29,12 @@ pub enum BlobCacheError { InvalidKey(String), } +impl BlobCacheError { + fn io_ctx(ctx: impl std::fmt::Display, e: impl std::fmt::Display) -> Self { + BlobCacheError::Io(format!("{ctx}: {e}")) + } +} + /// Content-addressed blob cache backed by cacache. pub struct BlobCache { cache_dir: PathBuf, @@ -51,7 +57,7 @@ impl BlobCache { // cacache creates the directory on first write, but we create // it eagerly so total_size() works before any writes. std::fs::create_dir_all(&cache_dir) - .map_err(|e| BlobCacheError::Io(format!("mkdir cache: {e}")))?; + .map_err(|e| BlobCacheError::io_ctx("mkdir cache", e))?; Ok(Self { cache_dir, max_bytes, @@ -73,7 +79,7 @@ impl BlobCache { let cache_key = format!("{prefix}{key}"); cacache::write(&self.cache_dir, &cache_key, data) .await - .map_err(|e| BlobCacheError::Io(format!("cacache write: {e}")))?; + .map_err(|e| BlobCacheError::io_ctx("cacache write", e))?; Ok(()) } @@ -106,7 +112,7 @@ impl BlobCache { let cache_key = format!("{prefix}{key}"); cacache::remove(&self.cache_dir, &cache_key) .await - .map_err(|e| BlobCacheError::Io(format!("cacache remove: {e}")))?; + .map_err(|e| BlobCacheError::io_ctx("cacache remove", e))?; Ok(()) } @@ -116,14 +122,14 @@ impl BlobCache { tokio::task::spawn_blocking(move || { if path.exists() { std::fs::remove_dir_all(&path) - .map_err(|e| BlobCacheError::Io(format!("clear cache: {e}")))?; + .map_err(|e| BlobCacheError::io_ctx("clear cache", e))?; std::fs::create_dir_all(&path) - .map_err(|e| BlobCacheError::Io(format!("recreate cache dir: {e}")))?; + .map_err(|e| BlobCacheError::io_ctx("recreate cache dir", e))?; } Ok(()) }) .await - .map_err(|e| BlobCacheError::Io(format!("clear task: {e}")))? + .map_err(|e| BlobCacheError::io_ctx("clear task", e))? } /// Return total bytes used by all blobs. @@ -148,7 +154,7 @@ impl BlobCache { Ok(total) }) .await - .map_err(|e| BlobCacheError::Io(format!("total_size task: {e}")))? + .map_err(|e| BlobCacheError::io_ctx("total_size task", e))? } /// Evict oldest entries by timestamp until total size is under @@ -198,7 +204,7 @@ impl BlobCache { Ok(()) }) .await - .map_err(|e| BlobCacheError::Io(format!("evict task: {e}")))? + .map_err(|e| BlobCacheError::io_ctx("evict task", e))? } } diff --git a/crates/chanora_protocol/src/lib.rs b/crates/chanora_protocol/src/lib.rs index 4d77ce6..9f7f0e1 100644 --- a/crates/chanora_protocol/src/lib.rs +++ b/crates/chanora_protocol/src/lib.rs @@ -120,3 +120,13 @@ pub enum ProtocolError { #[error("file transfer failed: {0}")] FileTransfer(String), } + +impl ProtocolError { + fn lost(msg: impl Into) -> Self { + ProtocolError::Lost(msg.into()) + } + + fn backend_ctx(ctx: impl std::fmt::Display, e: impl std::fmt::Display) -> Self { + ProtocolError::Backend(format!("{ctx}: {e}")) + } +} diff --git a/crates/chanora_storage/src/lib.rs b/crates/chanora_storage/src/lib.rs index 1a31609..4d4471c 100644 --- a/crates/chanora_storage/src/lib.rs +++ b/crates/chanora_storage/src/lib.rs @@ -79,8 +79,26 @@ pub enum StorageError { Crypto(String), } +impl StorageError { + fn io_ctx(ctx: impl std::fmt::Display, e: impl std::fmt::Display) -> Self { + StorageError::Io(format!("{ctx}: {e}")) + } + + fn crypto_ctx(ctx: impl std::fmt::Display, e: impl std::fmt::Display) -> Self { + StorageError::Crypto(format!("{ctx}: {e}")) + } + + fn sqlite_ctx(ctx: impl std::fmt::Display, e: impl std::fmt::Display) -> Self { + StorageError::Sqlite(format!("{ctx}: {e}")) + } + + fn migration_ctx(ctx: impl std::fmt::Display, e: impl std::fmt::Display) -> Self { + StorageError::Migration(format!("{ctx}: {e}")) + } +} + fn ensure_dir(dir: &Path) -> Result<(), StorageError> { - fs::create_dir_all(dir).map_err(|e| StorageError::Io(format!("mkdir {dir:?}: {e}"))) + fs::create_dir_all(dir).map_err(|e| StorageError::io_ctx(format!("mkdir {dir:?}"), e)) } /// Audio-related per-identity settings persisted alongside the @@ -246,7 +264,7 @@ impl IdentityFileStore { Ok(b64) => { let bytes = base64::engine::general_purpose::STANDARD .decode(b64.as_bytes()) - .map_err(|e| StorageError::Crypto(format!("keyring dek decode: {e}")))?; + .map_err(|e| StorageError::crypto_ctx("keyring dek decode", e))?; if bytes.len() != 32 { return Err(StorageError::Crypto(format!( "keyring dek length {} (expected 32)", @@ -359,9 +377,9 @@ impl IdentityFileStore { let _ = self.keyring_save(&key); let mut f = open_private(&self.dek_path)?; f.write_all(&key) - .map_err(|e| StorageError::Io(format!("write dek: {e}")))?; + .map_err(|e| StorageError::io_ctx("write dek", e))?; f.sync_all() - .map_err(|e| StorageError::Io(format!("sync dek: {e}")))?; + .map_err(|e| StorageError::io_ctx("sync dek", e))?; info!(target: "chanora_storage", path = ?self.dek_path, "DEK generated (file fallback)"); key.zeroize(); Ok(()) @@ -397,11 +415,11 @@ impl IdentityFileStore { let mut f = match fs::File::open(&self.path) { Ok(f) => f, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(e) => return Err(StorageError::Io(format!("open {:?}: {e}", self.path))), + Err(e) => return Err(StorageError::io_ctx(format!("open {:?}", self.path), e)), }; let mut buf = Vec::new(); f.read_to_end(&mut buf) - .map_err(|e| StorageError::Io(format!("read {:?}: {e}", self.path)))?; + .map_err(|e| StorageError::io_ctx(format!("read {:?}", self.path), e))?; if buf.is_empty() { return Ok(None); } @@ -420,11 +438,11 @@ impl IdentityFileStore { let nonce = Nonce::from_slice(nonce_bytes); let pt = cipher.decrypt(nonce, &buf[12..]).map_err(|e| { key_bytes.zeroize(); - StorageError::Crypto(format!("decrypt: {e}")) + StorageError::crypto_ctx("decrypt", e) })?; key_bytes.zeroize(); let s = String::from_utf8(pt) - .map_err(|e| StorageError::Crypto(format!("plaintext not utf8: {e}")))?; + .map_err(|e| StorageError::crypto_ctx("plaintext not utf8", e))?; let trimmed = s.trim().to_string(); if trimmed.is_empty() { return Ok(None); @@ -440,7 +458,7 @@ impl IdentityFileStore { "identity file is in legacy plaintext format; will encrypt on next save" ); let s = String::from_utf8(buf) - .map_err(|e| StorageError::Io(format!("legacy not utf8: {e}")))?; + .map_err(|e| StorageError::io_ctx("legacy not utf8", e))?; let trimmed = s.trim().to_string(); if trimmed.is_empty() { Ok(None) @@ -462,7 +480,7 @@ impl IdentityFileStore { let nonce = Nonce::from_slice(&nonce_bytes); let ct = cipher.encrypt(nonce, plaintext).map_err(|e| { key_bytes.zeroize(); - StorageError::Crypto(format!("encrypt: {e}")) + StorageError::crypto_ctx("encrypt", e) })?; key_bytes.zeroize(); @@ -471,14 +489,14 @@ impl IdentityFileStore { { let mut f = open_private(&tmp)?; f.write_all(&nonce_bytes) - .map_err(|e| StorageError::Io(format!("write nonce: {e}")))?; + .map_err(|e| StorageError::io_ctx("write nonce", e))?; f.write_all(&ct) - .map_err(|e| StorageError::Io(format!("write ct: {e}")))?; + .map_err(|e| StorageError::io_ctx("write ct", e))?; f.sync_all() - .map_err(|e| StorageError::Io(format!("sync {tmp:?}: {e}")))?; + .map_err(|e| StorageError::io_ctx(format!("sync {tmp:?}"), e))?; } fs::rename(&tmp, &self.path) - .map_err(|e| StorageError::Io(format!("rename {tmp:?} -> {:?}: {e}", self.path)))?; + .map_err(|e| StorageError::io_ctx(format!("rename {tmp:?} -> {:?}", self.path), e))?; info!(target: "chanora_storage", path = ?self.path, "identity persisted (encrypted)"); Ok(()) } @@ -507,17 +525,17 @@ impl IdentityFileStore { let path = self.meta_path(); let tmp = path.with_extension("json.tmp"); let body = serde_json::to_vec_pretty(m) - .map_err(|e| StorageError::Io(format!("meta serialize: {e}")))?; + .map_err(|e| StorageError::io_ctx("meta serialize", e))?; { let mut f = fs::File::create(&tmp) - .map_err(|e| StorageError::Io(format!("open meta {tmp:?}: {e}")))?; + .map_err(|e| StorageError::io_ctx(format!("open meta {tmp:?}"), e))?; f.write_all(&body) - .map_err(|e| StorageError::Io(format!("write meta: {e}")))?; + .map_err(|e| StorageError::io_ctx("write meta", e))?; f.sync_all() - .map_err(|e| StorageError::Io(format!("sync meta: {e}")))?; + .map_err(|e| StorageError::io_ctx("sync meta", e))?; } fs::rename(&tmp, &path) - .map_err(|e| StorageError::Io(format!("rename meta {tmp:?} -> {path:?}: {e}")))?; + .map_err(|e| StorageError::io_ctx(format!("rename meta {tmp:?} -> {path:?}"), e))?; Ok(()) } @@ -623,10 +641,10 @@ fn is_plausibly_legacy_plaintext(buf: &[u8]) -> bool { /// and the legacy migration path inside `ensure_dek`. fn read_file_dek(path: &Path) -> Result<[u8; 32], StorageError> { let mut f = - fs::File::open(path).map_err(|e| StorageError::Io(format!("open dek {:?}: {e}", path)))?; + fs::File::open(path).map_err(|e| StorageError::io_ctx(format!("open dek {path:?}"), e))?; let mut key = [0u8; 32]; f.read_exact(&mut key) - .map_err(|e| StorageError::Io(format!("read dek: {e}")))?; + .map_err(|e| StorageError::io_ctx("read dek", e))?; Ok(key) } @@ -656,7 +674,7 @@ fn open_private(p: &Path) -> Result { .truncate(true) .mode(0o600) .open(p) - .map_err(|e| StorageError::Io(format!("open {p:?}: {e}"))) + .map_err(|e| StorageError::io_ctx(format!("open {p:?}"), e)) } #[cfg(not(unix))] @@ -670,7 +688,7 @@ fn open_private(p: &Path) -> Result { .write(true) .truncate(true) .open(p) - .map_err(|e| StorageError::Io(format!("open {p:?}: {e}"))) + .map_err(|e| StorageError::io_ctx(format!("open {p:?}"), e)) } /// Public abstraction over the per-install envelope-encryption @@ -725,7 +743,7 @@ impl DekCrypto { let nonce = Nonce::from_slice(&nonce_bytes); let ct = cipher .encrypt(nonce, plaintext) - .map_err(|e| StorageError::Crypto(format!("encrypt: {e}")))?; + .map_err(|e| StorageError::crypto_ctx("encrypt", e))?; let mut out = Vec::with_capacity(12 + ct.len()); out.extend_from_slice(&nonce_bytes); out.extend_from_slice(&ct); @@ -744,7 +762,7 @@ impl DekCrypto { let nonce = Nonce::from_slice(&blob[..12]); cipher .decrypt(nonce, &blob[12..]) - .map_err(|e| StorageError::Crypto(format!("decrypt: {e}"))) + .map_err(|e| StorageError::crypto_ctx("decrypt", e)) } } @@ -821,9 +839,9 @@ impl BookmarkRepository { ensure_dir(dir)?; let path = dir.join("chanora.db"); let conn = Connection::open(&path) - .map_err(|e| StorageError::Sqlite(format!("open {path:?}: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx(format!("open {path:?}"), e))?; conn.pragma_update(None, "foreign_keys", "ON") - .map_err(|e| StorageError::Sqlite(format!("pragma: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("pragma", e))?; conn.execute_batch( "CREATE TABLE IF NOT EXISTS bookmarks ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -837,7 +855,7 @@ impl BookmarkRepository { ); INSERT OR IGNORE INTO schema_version(v) VALUES (1);", ) - .map_err(|e| StorageError::Migration(format!("init schema: {e}")))?; + .map_err(|e| StorageError::migration_ctx("init schema", e))?; // Schema v2 migration: encrypted password column. Idempotent. let has_blob: i64 = conn .query_row( @@ -845,12 +863,12 @@ impl BookmarkRepository { [], |r| r.get(0), ) - .map_err(|e| StorageError::Migration(format!("table_info: {e}")))?; + .map_err(|e| StorageError::migration_ctx("table_info", e))?; if has_blob == 0 { conn.execute("ALTER TABLE bookmarks ADD COLUMN password_blob BLOB", []) - .map_err(|e| StorageError::Migration(format!("add password_blob: {e}")))?; + .map_err(|e| StorageError::migration_ctx("add password_blob", e))?; conn.execute("INSERT OR IGNORE INTO schema_version(v) VALUES (2)", []) - .map_err(|e| StorageError::Migration(format!("bump version: {e}")))?; + .map_err(|e| StorageError::migration_ctx("bump version", e))?; info!(target: "chanora_storage", "bookmark db migrated to v2 (password_blob)"); } info!(target: "chanora_storage", path = ?path, "bookmark db opened"); @@ -886,7 +904,7 @@ impl BookmarkRepository { "INSERT INTO bookmarks (display_name, host, nickname, password, password_blob) VALUES (?1, ?2, ?3, ?4, ?5)", params![b.display_name, b.host, b.nickname, plain, blob], ) - .map_err(|e| StorageError::Sqlite(format!("insert: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("insert", e))?; Ok(conn.last_insert_rowid()) } @@ -915,20 +933,20 @@ impl BookmarkRepository { |row| row.get(0), ) .optional() - .map_err(|e| StorageError::Sqlite(format!("select: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("select", e))?; if let Some(id) = existing { conn.execute( "UPDATE bookmarks SET nickname = ?1, password = ?2, password_blob = ?3 WHERE id = ?4", params![b.nickname, plain, blob, id], ) - .map_err(|e| StorageError::Sqlite(format!("update: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("update", e))?; Ok(id) } else { conn.execute( "INSERT INTO bookmarks (display_name, host, nickname, password, password_blob) VALUES (?1, ?2, ?3, ?4, ?5)", params![b.display_name, b.host, b.nickname, plain, blob], ) - .map_err(|e| StorageError::Sqlite(format!("insert: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("insert", e))?; Ok(conn.last_insert_rowid()) } } @@ -957,7 +975,7 @@ impl BookmarkRepository { "UPDATE bookmarks SET display_name=?1, host=?2, nickname=?3, password=?4, password_blob=?5 WHERE id=?6", params![b.display_name, b.host, b.nickname, plain, blob, b.id], ) - .map_err(|e| StorageError::Sqlite(format!("update: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("update", e))?; if n == 0 { Err(StorageError::NotFound) } else { @@ -972,7 +990,7 @@ impl BookmarkRepository { .lock() .map_err(|_| StorageError::Sqlite("poisoned lock".to_string()))?; conn.execute("DELETE FROM bookmarks WHERE id = ?1", params![id]) - .map_err(|e| StorageError::Sqlite(format!("delete: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("delete", e))?; Ok(()) } @@ -988,7 +1006,7 @@ impl BookmarkRepository { .prepare( "SELECT id, display_name, host, nickname, password, password_blob FROM bookmarks ORDER BY id", ) - .map_err(|e| StorageError::Sqlite(format!("prepare: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("prepare", e))?; let rows = stmt .query_map([], |row| { let id: i64 = row.get(0)?; @@ -999,15 +1017,15 @@ impl BookmarkRepository { let blob: Option> = row.get(5)?; Ok((id, display_name, host, nickname, plain, blob)) }) - .map_err(|e| StorageError::Sqlite(format!("query: {e}")))?; + .map_err(|e| StorageError::sqlite_ctx("query", e))?; let mut out = Vec::new(); for r in rows { let (id, display_name, host, nickname, plain, blob) = - r.map_err(|e| StorageError::Sqlite(format!("row: {e}")))?; + r.map_err(|e| StorageError::sqlite_ctx("row", e))?; let password = match (blob.as_ref(), self.crypto.as_ref()) { (Some(b), Some(c)) => Some( String::from_utf8(c.decrypt(b)?) - .map_err(|e| StorageError::Crypto(format!("blob utf8: {e}")))?, + .map_err(|e| StorageError::crypto_ctx("blob utf8", e))?, ), (Some(_), None) => { // We have an encrypted blob but no key. Skip the