refactor: split the library into fumi-core and make it embeddable
This commit is contained in:
parent
d3cd0afb4f
commit
8fb5661b53
28 changed files with 975 additions and 724 deletions
193
core/src/error.rs
Normal file
193
core/src/error.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
//! 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.
|
||||
NotPinned { host: String },
|
||||
/// A pinned server presented a different key (sec 4): a hard abort, the
|
||||
/// pin is the entire trust model.
|
||||
PinMismatch {
|
||||
host: String,
|
||||
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,
|
||||
},
|
||||
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 } => write!(f, "no pinned key for {host}"),
|
||||
Self::PinMismatch {
|
||||
host,
|
||||
pinned,
|
||||
presented,
|
||||
} => write!(
|
||||
f,
|
||||
"{host} presented a different key than the one pinned\n pinned: {}\n presented: {}",
|
||||
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"),
|
||||
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::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, .. } => Some(source),
|
||||
Self::Storage(e) => Some(e),
|
||||
Self::Noise(e) => Some(e),
|
||||
Self::Io(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue