use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("i/o error: {0}")]
Io(#[from] std::io::Error),
#[error("botan: {0}")]
Botan(String),
#[error("hex: {0}")]
Hex(String),
#[error("base64: {0}")]
Base64(String),
#[error("cipher: {0}")]
Cipher(String),
#[error("unknown cipher algorithm: {alg}")]
CipherUnknown { alg: String },
#[error("pbkdf: {0}")]
Pbkdf(String),
#[error("policy violation: {0}")]
Policy(String),
#[error("capability policy '{rule}' violated: {context}")]
PolicyViolation { rule: String, context: String },
#[error("parse error in {file}:{lineno}: {msg}")]
Parse {
file: String,
lineno: i32,
msg: String,
},
#[error("CAS: {0}")]
Cas(String),
#[error("CAS hash invalid: {hash}")]
CasHashInvalid { hash: String },
#[error("CAS hash mismatch: expected {expected}, computed {actual}")]
CasHashMismatch { expected: String, actual: String },
#[error("CAS blob not found: {hash}")]
CasNotFound { hash: String },
#[error("CAS operation '{op}' not supported by this backend")]
CasUnsupported { op: &'static str },
#[error("PHC: {0}")]
Phc(String),
#[error("JSON: {0}")]
Json(String),
#[error("{0}")]
Msg(String),
#[error("invalid argument {arg}: {reason}")]
InvalidArg { arg: &'static str, reason: String },
#[error("extfield {field} malformed: {reason}")]
Extfield { field: &'static str, reason: String },
#[error("signature verification failed for {key_id}")]
SignatureVerify { key_id: String },
#[error("block {word} shape error: {reason}")]
BlockShape { word: String, reason: String },
#[error("conflict resolution failed for {word}: {reason}")]
ConflictResolve { word: String, reason: String },
}
impl Error {
pub fn botan(e: impl std::fmt::Display) -> Self {
Error::Botan(e.to_string())
}
pub fn msg(s: impl Into<String>) -> Self {
Error::Msg(s.into())
}
pub fn json(e: impl std::fmt::Display) -> Self {
Error::Json(e.to_string())
}
pub fn with_context(self, ctx: impl std::fmt::Display) -> Self {
Error::Msg(format!("{}: {}", ctx, self))
}
}
impl From<botan::Error> for Error {
fn from(e: botan::Error) -> Self {
Error::Botan(e.to_string())
}
}
impl From<hex::FromHexError> for Error {
fn from(e: hex::FromHexError) -> Self {
Error::Hex(e.to_string())
}
}
impl From<crate::ledger::DagError> for Error {
fn from(e: crate::ledger::DagError) -> Self {
Error::msg(e.to_string())
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_invalid_arg() {
let e = Error::InvalidArg {
arg: "--word",
reason: "missing".to_string(),
};
assert_eq!(e.to_string(), "invalid argument --word: missing");
}
#[test]
fn display_extfield() {
let e = Error::Extfield {
field: "payload",
reason: "CHAIN missing required 'payload' field".to_string(),
};
assert_eq!(
e.to_string(),
"extfield payload malformed: CHAIN missing required 'payload' field"
);
}
#[test]
fn display_signature_verify() {
let e = Error::SignatureVerify {
key_id: "ed25519:abcd".to_string(),
};
assert_eq!(
e.to_string(),
"signature verification failed for ed25519:abcd"
);
}
#[test]
fn display_block_shape() {
let e = Error::BlockShape {
word: "SECRET".to_string(),
reason: "ENCRYPTED block has no DATA or STORED child".to_string(),
};
assert_eq!(
e.to_string(),
"block SECRET shape error: ENCRYPTED block has no DATA or STORED child"
);
}
#[test]
fn display_conflict_resolve() {
let e = Error::ConflictResolve {
word: "AGENT".to_string(),
reason: "no resolution strategy picked".to_string(),
};
assert_eq!(
e.to_string(),
"conflict resolution failed for AGENT: no resolution strategy picked"
);
}
#[test]
fn display_io() {
let e = Error::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
assert!(e.to_string().contains("i/o error"));
}
#[test]
fn display_botan() {
let e = Error::Botan("rng failure".to_string());
assert_eq!(e.to_string(), "botan: rng failure");
}
#[test]
fn display_hex() {
let e = Error::Hex("odd-length".to_string());
assert_eq!(e.to_string(), "hex: odd-length");
}
#[test]
fn display_base64() {
let e = Error::Base64("invalid char".to_string());
assert_eq!(e.to_string(), "base64: invalid char");
}
#[test]
fn display_cipher() {
let e = Error::Cipher("wrong key length".to_string());
assert_eq!(e.to_string(), "cipher: wrong key length");
}
#[test]
fn display_pbkdf() {
let e = Error::Pbkdf("iterations below floor".to_string());
assert_eq!(e.to_string(), "pbkdf: iterations below floor");
}
#[test]
fn display_policy() {
let e = Error::Policy("sha1 not approved".to_string());
assert_eq!(e.to_string(), "policy violation: sha1 not approved");
}
#[test]
fn display_policy_violation() {
let e = Error::PolicyViolation {
rule: "trust_root".to_string(),
context: "signer abc not in trust_roots".to_string(),
};
assert_eq!(
e.to_string(),
"capability policy 'trust_root' violated: signer abc not in trust_roots"
);
}
#[test]
fn display_parse() {
let e = Error::Parse {
file: "test.ept".to_string(),
lineno: 42,
msg: "unexpected END".to_string(),
};
assert_eq!(e.to_string(), "parse error in test.ept:42: unexpected END");
}
#[test]
fn display_cas() {
let e = Error::Cas("hash mismatch".to_string());
assert_eq!(e.to_string(), "CAS: hash mismatch");
}
#[test]
fn display_phc() {
let e = Error::Phc("missing $ separator".to_string());
assert_eq!(e.to_string(), "PHC: missing $ separator");
}
#[test]
fn display_json() {
let e = Error::Json("unexpected EOF".to_string());
assert_eq!(e.to_string(), "JSON: unexpected EOF");
}
#[test]
fn display_msg() {
let e = Error::Msg("something happened".to_string());
assert_eq!(e.to_string(), "something happened");
}
#[test]
fn display_cas_hash_invalid() {
let e = Error::CasHashInvalid {
hash: "xyz".to_string(),
};
assert_eq!(e.to_string(), "CAS hash invalid: xyz");
}
#[test]
fn display_cas_hash_mismatch() {
let e = Error::CasHashMismatch {
expected: "aaa".to_string(),
actual: "bbb".to_string(),
};
assert_eq!(
e.to_string(),
"CAS hash mismatch: expected aaa, computed bbb"
);
}
#[test]
fn display_cas_not_found() {
let e = Error::CasNotFound {
hash: "abc123".to_string(),
};
assert_eq!(e.to_string(), "CAS blob not found: abc123");
}
#[test]
fn display_cas_unsupported() {
let e = Error::CasUnsupported { op: "list" };
assert_eq!(
e.to_string(),
"CAS operation 'list' not supported by this backend"
);
}
#[test]
fn display_cipher_unknown() {
let e = Error::CipherUnknown {
alg: "aes-999".to_string(),
};
assert_eq!(e.to_string(), "unknown cipher algorithm: aes-999");
}
}