refactor: reduce map_err boilerplate with context helper methods (TODO-025)

Add per-crate variant_ctx() helper methods to StorageError, BlobCacheError,
ProtocolError, and BridgeError. Replace 78 .map_err(|e| format!(...))
closures with concise context calls. Identical error messages preserved.
This commit is contained in:
Edison Jwa
2026-06-11 12:25:00 +09:00
parent 1efeaac19d
commit 40883c40c8
4 changed files with 89 additions and 49 deletions
+6
View File
@@ -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<chanora_core::CoreError> for BridgeError {
fn from(e: chanora_core::CoreError) -> Self {
match e {
+14 -8
View File
@@ -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))?
}
}
+10
View File
@@ -120,3 +120,13 @@ pub enum ProtocolError {
#[error("file transfer failed: {0}")]
FileTransfer(String),
}
impl ProtocolError {
fn lost(msg: impl Into<String>) -> Self {
ProtocolError::Lost(msg.into())
}
fn backend_ctx(ctx: impl std::fmt::Display, e: impl std::fmt::Display) -> Self {
ProtocolError::Backend(format!("{ctx}: {e}"))
}
}
+59 -41
View File
@@ -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<fs::File, StorageError> {
.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<fs::File, StorageError> {
.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<Vec<u8>> = 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