use std::fmt::{Display, Formatter};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ErrorContext {
AssociatedData,
AeadCiphertext,
AeadEnvelope,
AuthenticationTag,
Blake2bKey,
Blake2bOutput,
Blake2b,
Box,
Ciphertext,
Curve25519PublicKey,
Data,
Ed25519PublicKey,
EphemeralPublicKey,
MemoryCost,
MemoryLimit,
Message,
Nonce,
OperationsLimit,
Output,
Parallelism,
Password,
PasswordHash,
PasswordHashAlgorithm,
PasswordHashMemoryCost,
PasswordHashParallelism,
PasswordHashSalt,
PasswordHashTimeCost,
PasswordHashVersion,
ProtectedMemory,
PublicKey,
SealedBox,
Secret,
SecretBox,
SecretKey,
Signature,
SignedMessage,
Slice,
Subkey,
Tag,
TimeCost,
}
impl Display for ErrorContext {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::AssociatedData => "associated data",
Self::AeadCiphertext => "AEAD ciphertext",
Self::AeadEnvelope => "AEAD envelope",
Self::AuthenticationTag => "authentication tag",
Self::Blake2bKey => "BLAKE2b key",
Self::Blake2bOutput => "BLAKE2b output",
Self::Blake2b => "BLAKE2b",
Self::Box => "box",
Self::Ciphertext => "ciphertext",
Self::Curve25519PublicKey => "Curve25519 public key",
Self::Data => "data",
Self::Ed25519PublicKey => "Ed25519 public key",
Self::EphemeralPublicKey => "ephemeral public key",
Self::MemoryCost => "memory cost",
Self::MemoryLimit => "memory limit",
Self::Message => "message",
Self::Nonce => "nonce",
Self::OperationsLimit => "operations limit",
Self::Output => "output",
Self::Parallelism => "parallelism",
Self::Password => "password",
Self::PasswordHash => "password hash",
Self::PasswordHashAlgorithm => "password hash algorithm",
Self::PasswordHashMemoryCost => "password hash memory cost",
Self::PasswordHashParallelism => "password hash parallelism",
Self::PasswordHashSalt => "password hash salt",
Self::PasswordHashTimeCost => "password hash time cost",
Self::PasswordHashVersion => "password hash version",
Self::ProtectedMemory => "protected memory",
Self::PublicKey => "public key",
Self::SealedBox => "sealed box",
Self::Secret => "secret",
Self::SecretBox => "secretbox",
Self::SecretKey => "secret key",
Self::Signature => "signature",
Self::SignedMessage => "signed message",
Self::Slice => "slice",
Self::Subkey => "subkey",
Self::Tag => "tag",
Self::TimeCost => "time cost",
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum LengthConstraint {
Exact(usize),
AtLeast(usize),
AtMost(usize),
Between { min: usize, max: usize },
}
impl Display for LengthConstraint {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Exact(expected) => write!(f, "exactly {expected}"),
Self::AtLeast(min) => write!(f, "at least {min}"),
Self::AtMost(max) => write!(f, "at most {max}"),
Self::Between { min, max } => write!(f, "between {min} and {max} (inclusive)"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ValueConstraint {
Between { min: u64, max: u64 },
AllowedBits { mask: u64 },
}
impl Display for ValueConstraint {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Between { min, max } => write!(f, "between {min} and {max} (inclusive)"),
Self::AllowedBits { mask } => {
write!(f, "a value containing only bits from mask 0x{mask:x}")
}
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
AuthenticationFailed,
InvalidLength {
context: ErrorContext,
actual: usize,
constraint: LengthConstraint,
},
InvalidValue {
context: ErrorContext,
actual: u64,
constraint: ValueConstraint,
},
InvalidEncoding {
context: ErrorContext,
},
InvalidKey {
context: ErrorContext,
},
MissingData {
context: ErrorContext,
},
InvalidState {
context: ErrorContext,
},
ArithmeticOverflow {
context: ErrorContext,
},
AllocationFailed {
context: ErrorContext,
},
Io(std::io::Error),
}
impl Error {
pub(crate) const fn invalid_encoding(context: ErrorContext) -> Self {
Self::InvalidEncoding { context }
}
pub(crate) const fn invalid_key(context: ErrorContext) -> Self {
Self::InvalidKey { context }
}
pub(crate) const fn missing_data(context: ErrorContext) -> Self {
Self::MissingData { context }
}
pub(crate) const fn invalid_state(context: ErrorContext) -> Self {
Self::InvalidState { context }
}
pub(crate) const fn arithmetic_overflow(context: ErrorContext) -> Self {
Self::ArithmeticOverflow { context }
}
pub(crate) const fn allocation_failed(context: ErrorContext) -> Self {
Self::AllocationFailed { context }
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::AuthenticationFailed => f.write_str("authentication failed"),
Self::InvalidLength {
context,
actual,
constraint,
} => write!(
f,
"invalid {context} length: expected {constraint}, got {actual}"
),
Self::InvalidValue {
context,
actual,
constraint,
} => write!(
f,
"invalid {context} value: expected {constraint}, got {actual}"
),
Self::InvalidEncoding { context } => write!(f, "invalid {context} encoding"),
Self::InvalidKey { context } => write!(f, "invalid {context}"),
Self::MissingData { context } => write!(f, "missing {context}"),
Self::InvalidState { context } => write!(f, "invalid {context} state"),
Self::ArithmeticOverflow { context } => {
write!(f, "arithmetic overflow while calculating {context} length")
}
Self::AllocationFailed { context } => {
write!(f, "unable to allocate memory for {context}")
}
Self::Io(error) => write!(f, "I/O error: {error}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
_ => None,
}
}
}
macro_rules! length_error {
($context:expr_2021, $actual:expr_2021,exact $expected:expr_2021) => {
crate::error::Error::InvalidLength {
context: $context,
actual: $actual,
constraint: crate::error::LengthConstraint::Exact($expected),
}
};
($context:expr_2021, $actual:expr_2021,min $min:expr_2021) => {
crate::error::Error::InvalidLength {
context: $context,
actual: $actual,
constraint: crate::error::LengthConstraint::AtLeast($min),
}
};
($context:expr_2021, $actual:expr_2021,max $max:expr_2021) => {
crate::error::Error::InvalidLength {
context: $context,
actual: $actual,
constraint: crate::error::LengthConstraint::AtMost($max),
}
};
($context:expr_2021, $actual:expr_2021,range $min:expr_2021, $max:expr_2021) => {
crate::error::Error::InvalidLength {
context: $context,
actual: $actual,
constraint: crate::error::LengthConstraint::Between {
min: $min,
max: $max,
},
}
};
}
macro_rules! validate_value {
($min:expr_2021, $max:expr_2021, $value:expr_2021, $context:expr_2021) => {
if !($min..=$max).contains(&$value) {
return Err(crate::error::Error::InvalidValue {
context: $context,
actual: $value as u64,
constraint: crate::error::ValueConstraint::Between {
min: $min as u64,
max: $max as u64,
},
});
}
};
}
macro_rules! validate_length {
(exact $expected:expr_2021, $value:expr_2021, $context:expr_2021) => {
if $value != $expected {
return Err(length_error!($context, $value, exact $expected));
}
};
($min:expr_2021, $max:expr_2021, $value:expr_2021, $context:expr_2021) => {
if !($min..=$max).contains(&$value) {
return Err(length_error!($context, $value, range $min, $max));
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contexts_have_clear_human_readable_names() {
let cases = [
(ErrorContext::AssociatedData, "associated data"),
(ErrorContext::AeadCiphertext, "AEAD ciphertext"),
(ErrorContext::AeadEnvelope, "AEAD envelope"),
(ErrorContext::AuthenticationTag, "authentication tag"),
(ErrorContext::Blake2bKey, "BLAKE2b key"),
(ErrorContext::Blake2bOutput, "BLAKE2b output"),
(ErrorContext::Blake2b, "BLAKE2b"),
(ErrorContext::Box, "box"),
(ErrorContext::Ciphertext, "ciphertext"),
(ErrorContext::Curve25519PublicKey, "Curve25519 public key"),
(ErrorContext::Data, "data"),
(ErrorContext::Ed25519PublicKey, "Ed25519 public key"),
(ErrorContext::EphemeralPublicKey, "ephemeral public key"),
(ErrorContext::MemoryCost, "memory cost"),
(ErrorContext::MemoryLimit, "memory limit"),
(ErrorContext::Message, "message"),
(ErrorContext::Nonce, "nonce"),
(ErrorContext::OperationsLimit, "operations limit"),
(ErrorContext::Output, "output"),
(ErrorContext::Parallelism, "parallelism"),
(ErrorContext::Password, "password"),
(ErrorContext::PasswordHash, "password hash"),
(
ErrorContext::PasswordHashAlgorithm,
"password hash algorithm",
),
(
ErrorContext::PasswordHashMemoryCost,
"password hash memory cost",
),
(
ErrorContext::PasswordHashParallelism,
"password hash parallelism",
),
(ErrorContext::PasswordHashSalt, "password hash salt"),
(
ErrorContext::PasswordHashTimeCost,
"password hash time cost",
),
(ErrorContext::PasswordHashVersion, "password hash version"),
(ErrorContext::ProtectedMemory, "protected memory"),
(ErrorContext::PublicKey, "public key"),
(ErrorContext::SealedBox, "sealed box"),
(ErrorContext::Secret, "secret"),
(ErrorContext::SecretBox, "secretbox"),
(ErrorContext::SecretKey, "secret key"),
(ErrorContext::Signature, "signature"),
(ErrorContext::SignedMessage, "signed message"),
(ErrorContext::Slice, "slice"),
(ErrorContext::Subkey, "subkey"),
(ErrorContext::Tag, "tag"),
(ErrorContext::TimeCost, "time cost"),
];
for (context, expected) in cases {
assert_eq!(context.to_string(), expected);
}
}
#[test]
fn constraints_describe_their_requirements() {
let length_cases = [
(LengthConstraint::Exact(4), "exactly 4"),
(LengthConstraint::AtLeast(4), "at least 4"),
(LengthConstraint::AtMost(4), "at most 4"),
(
LengthConstraint::Between { min: 2, max: 4 },
"between 2 and 4 (inclusive)",
),
];
for (constraint, expected) in length_cases {
assert_eq!(constraint.to_string(), expected);
}
let value_cases = [
(
ValueConstraint::Between { min: 2, max: 4 },
"between 2 and 4 (inclusive)",
),
(
ValueConstraint::AllowedBits { mask: 0x3 },
"a value containing only bits from mask 0x3",
),
];
for (constraint, expected) in value_cases {
assert_eq!(constraint.to_string(), expected);
}
}
#[test]
fn display_is_human_readable_without_source_locations() {
let cases = [
(Error::AuthenticationFailed, "authentication failed"),
(
Error::InvalidLength {
context: ErrorContext::Nonce,
actual: 12,
constraint: LengthConstraint::Exact(24),
},
"invalid nonce length: expected exactly 24, got 12",
),
(
Error::InvalidLength {
context: ErrorContext::Blake2bOutput,
actual: 0,
constraint: LengthConstraint::Between { min: 1, max: 64 },
},
"invalid BLAKE2b output length: expected between 1 and 64 (inclusive), got 0",
),
(
Error::InvalidValue {
context: ErrorContext::Parallelism,
actual: 8,
constraint: ValueConstraint::Between { min: 1, max: 4 },
},
"invalid parallelism value: expected between 1 and 4 (inclusive), got 8",
),
(
Error::InvalidValue {
context: ErrorContext::Tag,
actual: 128,
constraint: ValueConstraint::AllowedBits { mask: 3 },
},
"invalid tag value: expected a value containing only bits from mask 0x3, got 128",
),
(
Error::InvalidEncoding {
context: ErrorContext::PasswordHashSalt,
},
"invalid password hash salt encoding",
),
(
Error::InvalidKey {
context: ErrorContext::Ed25519PublicKey,
},
"invalid Ed25519 public key",
),
(
Error::MissingData {
context: ErrorContext::EphemeralPublicKey,
},
"missing ephemeral public key",
),
(
Error::InvalidState {
context: ErrorContext::Blake2b,
},
"invalid BLAKE2b state",
),
(
Error::ArithmeticOverflow {
context: ErrorContext::Ciphertext,
},
"arithmetic overflow while calculating ciphertext length",
),
(
Error::AllocationFailed {
context: ErrorContext::MemoryCost,
},
"unable to allocate memory for memory cost",
),
];
for (error, expected) in cases {
assert_eq!(error.to_string(), expected);
}
}
#[test]
fn debug_is_structured_and_does_not_include_internal_source_locations() {
let error = Error::InvalidLength {
context: ErrorContext::Ciphertext,
actual: 7,
constraint: LengthConstraint::AtLeast(16),
};
assert_eq!(
format!("{error:?}"),
"InvalidLength { context: Ciphertext, actual: 7, constraint: AtLeast(16) }"
);
}
#[test]
fn wrapped_errors_preserve_their_source() {
use std::error::Error as _;
let error = Error::from(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"access denied",
));
assert_eq!(error.to_string(), "I/O error: access denied");
let debug = format!("{error:?}");
assert!(debug.contains("Io"));
assert!(debug.contains("PermissionDenied"));
assert!(debug.contains("access denied"));
assert!(error.source().is_some());
assert!(Error::AuthenticationFailed.source().is_none());
}
}