fumi/core/src/error.rs

239 lines
8.3 KiB
Rust
Raw Normal View History

//! The failure type embedders match on, and the status-code mapping it is
//! built from.
use std::fmt;
use crate::crypto::{b32, KEY_LEN};
use crate::transport::MAX_CHAIN;
/// Every failure the library reports, distinguished the way a caller needs
/// to decide what to do — no prose parsing. Each variant documents the
/// decision it supports; `Display` still produces a user-readable message.
#[derive(Debug)]
pub enum Error {
/// The wire status codes of SPEC.md sec 12, one variant each, carrying
/// what was being attempted. `UnknownStatus` is an unassigned code.
Malformed(String),
BadVersion(String),
UnknownUser(String),
AuthRequired(String),
AuthFailed(String),
QuotaExceeded(String),
TooLarge(String),
RateLimited(String),
NotPermitted(String),
InternalError(String),
UnknownStatus(u8, String),
/// The host has no pinned server key, so the session cannot be
/// authenticated (sec 4). A GUI routes this to pinning; an auth failure
/// is terminal. The port scopes the pin (address::trust_label).
2026-09-30 21:46:41 +03:00
NotPinned {
host: String,
port: u16,
},
/// A pinned server presented a different key (sec 4): a hard abort, the
/// pin is the entire trust model. The port scopes the pin
/// (address::trust_label).
PinMismatch {
host: String,
port: u16,
pinned: [u8; KEY_LEN],
presented: [u8; KEY_LEN],
},
/// A contact's key changed with no valid rotation chain (sec 7): the
/// user confirms out of band and imports, rather than clicking through.
KeyChanged {
address: String,
known: [u8; KEY_LEN],
offered: [u8; KEY_LEN],
},
/// The store has no account yet; registering or restoring creates one.
NotRegistered,
/// The rotation index would exceed the chain limit (sec 2).
2026-09-30 21:46:41 +03:00
ChainLimit {
index: u32,
},
/// The store was written by a schema this build cannot read; a host
/// decides when to migrate, not the library.
2026-09-30 21:46:41 +03:00
SchemaVersion {
found: i32,
expected: i32,
},
/// The server could not be reached at all.
Unreachable {
host: String,
port: u16,
source: std::io::Error,
},
/// The server dropped the connection mid-handshake after bounded
/// retries. A rate limiter drops connections without a reply, so this
/// is indistinguishable from a dying server; the decision it supports
/// is "retry later", not "treat the server as dead".
HandshakeRefused {
host: String,
port: u16,
source: std::io::Error,
attempts: usize,
},
#[cfg(feature = "store")]
Storage(rusqlite::Error),
Noise(snow::Error),
Io(std::io::Error),
/// Validation prose without a decision-relevant variant of its own.
Other(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Malformed(what) => write!(f, "{what} failed: malformed (1)"),
Self::BadVersion(what) => write!(f, "{what} failed: bad version (2)"),
Self::UnknownUser(what) => write!(f, "{what} failed: unknown user (3)"),
Self::AuthRequired(what) => write!(f, "{what} failed: auth required (4)"),
Self::AuthFailed(what) => write!(f, "{what} failed: auth failed (5)"),
Self::QuotaExceeded(what) => write!(f, "{what} failed: quota exceeded (6)"),
Self::TooLarge(what) => write!(f, "{what} failed: too large (7)"),
Self::RateLimited(what) => write!(f, "{what} failed: rate limited (8)"),
Self::NotPermitted(what) => write!(f, "{what} failed: not permitted (9)"),
Self::InternalError(what) => write!(f, "{what} failed: internal error (10)"),
Self::UnknownStatus(status, what) => {
write!(f, "{what} failed: unknown status {status}")
}
Self::NotPinned { host, port } => write!(
f,
"no pinned key for {}",
crate::address::trust_label(host, *port)
),
Self::PinMismatch {
host,
port,
pinned,
presented,
} => write!(
f,
"{} presented a different key than the one pinned\n pinned: {}\n presented: {}",
crate::address::trust_label(host, *port),
b32(pinned),
b32(presented)
),
Self::KeyChanged {
address,
known,
offered,
} => write!(
f,
"{address} presents a different key with no valid rotation chain.\n known: {}\n offered: {}",
b32(known),
b32(offered)
),
Self::NotRegistered => write!(
f,
"not registered; this store has no account (registering or restoring creates one)"
),
Self::ChainLimit { index } => write!(
f,
"rotation index {index} exceeds the chain limit of {MAX_CHAIN}"
),
Self::SchemaVersion { found, expected } => write!(
f,
"store schema version {found} is not this build's {expected}"
),
Self::Unreachable { host, port, source } => {
write!(f, "cannot reach {host}:{port}: {source}")
}
Self::HandshakeRefused {
host,
port,
source,
attempts,
} => write!(
f,
"{host}:{port} refused the connection mid-handshake after {attempts} attempt(s) ({source})\n \
possibly rate limited; a limiter drops connections without a reply, so a dying server looks the same"
),
#[cfg(feature = "store")]
Self::Storage(e) => write!(f, "storage error: {e}"),
Self::Noise(e) => write!(f, "noise error: {e}"),
Self::Io(e) => write!(f, "{e}"),
Self::Other(msg) => write!(f, "{msg}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Unreachable { source, .. } | Self::HandshakeRefused { source, .. } => {
Some(source)
}
#[cfg(feature = "store")]
Self::Storage(e) => Some(e),
Self::Noise(e) => Some(e),
Self::Io(e) => Some(e),
_ => None,
}
}
}
#[cfg(feature = "store")]
impl From<rusqlite::Error> for Error {
fn from(e: rusqlite::Error) -> Self {
Error::Storage(e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
impl From<snow::Error> for Error {
fn from(e: snow::Error) -> Self {
Error::Noise(e)
}
}
/// Status codes from SPEC.md sec 12. Unassigned numbers map to None, so a
/// caller cannot accidentally name one that does not exist.
pub fn status_name(status: u8) -> Option<&'static str> {
Some(match status {
0 => "ok",
1 => "malformed",
2 => "bad version",
3 => "unknown user",
4 => "auth required",
5 => "auth failed",
6 => "quota exceeded",
7 => "too large",
8 => "rate limited",
9 => "not permitted",
10 => "internal error",
_ => return None,
})
}
/// Turns a non-OK response into an error carrying the status code. The
/// optional reason string in the response body is never parsed (SPEC.md sec
/// 12); an unassigned code is surfaced by number and treated as a plain
/// failure.
pub fn expect_ok(status: u8, what: &str) -> Result<(), Error> {
if status == 0 {
return Ok(());
}
let what = what.to_string();
Err(match status {
1 => Error::Malformed(what),
2 => Error::BadVersion(what),
3 => Error::UnknownUser(what),
4 => Error::AuthRequired(what),
5 => Error::AuthFailed(what),
6 => Error::QuotaExceeded(what),
7 => Error::TooLarge(what),
8 => Error::RateLimited(what),
9 => Error::NotPermitted(what),
10 => Error::InternalError(what),
status => Error::UnknownStatus(status, what),
})
}