//! 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). 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). ChainLimit { index: u32, }, /// The store was written by a schema this build cannot read; a host /// decides when to migrate, not the library. 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 for Error { fn from(e: rusqlite::Error) -> Self { Error::Storage(e) } } impl From for Error { fn from(e: std::io::Error) -> Self { Error::Io(e) } } impl From 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), }) }