#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
InvalidBase64,
InvalidJson(&'static str),
InvalidHeader(&'static str),
UnsupportedAlgorithm,
AlgorithmNotAllowed,
InvalidKey(&'static str),
KeyAlgorithmMismatch,
InvalidSignature,
DecryptionFailed,
UnknownCritical,
TokenExpired,
TokenNotYetValid,
InvalidClaim(&'static str),
RandomnessUnavailable,
InvalidFormat(&'static str),
NoMatchingKey,
MultipleMatchingKeys,
}
impl Error {
pub const CODES: &'static [&'static str] = &[
"InvalidBase64",
"InvalidJson",
"InvalidHeader",
"UnsupportedAlgorithm",
"AlgorithmNotAllowed",
"InvalidKey",
"KeyAlgorithmMismatch",
"InvalidSignature",
"DecryptionFailed",
"UnknownCritical",
"TokenExpired",
"TokenNotYetValid",
"InvalidClaim",
"RandomnessUnavailable",
"InvalidFormat",
"NoMatchingKey",
"MultipleMatchingKeys",
];
pub fn code(&self) -> &'static str {
match self {
Error::InvalidBase64 => "InvalidBase64",
Error::InvalidJson(_) => "InvalidJson",
Error::InvalidHeader(_) => "InvalidHeader",
Error::UnsupportedAlgorithm => "UnsupportedAlgorithm",
Error::AlgorithmNotAllowed => "AlgorithmNotAllowed",
Error::InvalidKey(_) => "InvalidKey",
Error::KeyAlgorithmMismatch => "KeyAlgorithmMismatch",
Error::InvalidSignature => "InvalidSignature",
Error::DecryptionFailed => "DecryptionFailed",
Error::UnknownCritical => "UnknownCritical",
Error::TokenExpired => "TokenExpired",
Error::TokenNotYetValid => "TokenNotYetValid",
Error::InvalidClaim(_) => "InvalidClaim",
Error::RandomnessUnavailable => "RandomnessUnavailable",
Error::InvalidFormat(_) => "InvalidFormat",
Error::NoMatchingKey => "NoMatchingKey",
Error::MultipleMatchingKeys => "MultipleMatchingKeys",
}
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::InvalidBase64 => f.write_str("invalid base64url"),
Error::InvalidJson(detail) => write!(f, "invalid JSON: {detail}"),
Error::InvalidHeader(detail) => write!(f, "invalid header: {detail}"),
Error::UnsupportedAlgorithm => f.write_str("unsupported algorithm"),
Error::AlgorithmNotAllowed => f.write_str("algorithm not allowed"),
Error::InvalidKey(detail) => write!(f, "invalid key: {detail}"),
Error::KeyAlgorithmMismatch => f.write_str("key does not match the algorithm"),
Error::InvalidSignature => f.write_str("signature verification failed"),
Error::DecryptionFailed => f.write_str("decryption failed"),
Error::UnknownCritical => f.write_str("unknown critical header parameter"),
Error::TokenExpired => f.write_str("token expired"),
Error::TokenNotYetValid => f.write_str("token not yet valid"),
Error::InvalidClaim(detail) => write!(f, "invalid claim: {detail}"),
Error::RandomnessUnavailable => f.write_str("randomness unavailable"),
Error::InvalidFormat(detail) => write!(f, "invalid format: {detail}"),
Error::NoMatchingKey => f.write_str("no matching key"),
Error::MultipleMatchingKeys => f.write_str("several keys match"),
}
}
}
impl std::error::Error for Error {}
pub type Result<T> = core::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
fn one_of_each() -> Vec<Error> {
vec![
Error::InvalidBase64,
Error::InvalidJson("syntax error"),
Error::InvalidHeader("missing \"alg\""),
Error::UnsupportedAlgorithm,
Error::AlgorithmNotAllowed,
Error::InvalidKey("RSA modulus is under 2048 bits"),
Error::KeyAlgorithmMismatch,
Error::InvalidSignature,
Error::DecryptionFailed,
Error::UnknownCritical,
Error::TokenExpired,
Error::TokenNotYetValid,
Error::InvalidClaim("aud"),
Error::RandomnessUnavailable,
Error::InvalidFormat("compact JWS must have three parts"),
Error::NoMatchingKey,
Error::MultipleMatchingKeys,
]
}
fn variant_index(e: &Error) -> usize {
match e {
Error::InvalidBase64 => 0,
Error::InvalidJson(_) => 1,
Error::InvalidHeader(_) => 2,
Error::UnsupportedAlgorithm => 3,
Error::AlgorithmNotAllowed => 4,
Error::InvalidKey(_) => 5,
Error::KeyAlgorithmMismatch => 6,
Error::InvalidSignature => 7,
Error::DecryptionFailed => 8,
Error::UnknownCritical => 9,
Error::TokenExpired => 10,
Error::TokenNotYetValid => 11,
Error::InvalidClaim(_) => 12,
Error::RandomnessUnavailable => 13,
Error::InvalidFormat(_) => 14,
Error::NoMatchingKey => 15,
Error::MultipleMatchingKeys => 16,
}
}
#[test]
fn codes_are_the_stable_names() {
let expected = [
"InvalidBase64",
"InvalidJson",
"InvalidHeader",
"UnsupportedAlgorithm",
"AlgorithmNotAllowed",
"InvalidKey",
"KeyAlgorithmMismatch",
"InvalidSignature",
"DecryptionFailed",
"UnknownCritical",
"TokenExpired",
"TokenNotYetValid",
"InvalidClaim",
"RandomnessUnavailable",
"InvalidFormat",
"NoMatchingKey",
"MultipleMatchingKeys",
];
let actual: Vec<&str> = one_of_each().iter().map(Error::code).collect();
assert_eq!(actual, expected);
}
#[test]
fn multiple_matching_keys_is_appended_last() {
assert_eq!(Error::CODES.len(), 17);
assert_eq!(Error::CODES[15], "NoMatchingKey");
assert_eq!(Error::CODES[16], "MultipleMatchingKeys");
assert_eq!(Error::MultipleMatchingKeys.code(), "MultipleMatchingKeys");
assert_eq!(
Error::MultipleMatchingKeys.to_string(),
"several keys match"
);
assert_eq!(
format!("{:?}", Error::MultipleMatchingKeys),
"MultipleMatchingKeys"
);
}
#[test]
fn one_of_each_covers_every_variant_once() {
let indices: Vec<usize> = one_of_each().iter().map(variant_index).collect();
let expected: Vec<usize> = (0..Error::CODES.len()).collect();
assert_eq!(indices, expected);
}
#[test]
fn codes_list_matches_variants() {
let from_variants: Vec<&str> = one_of_each().iter().map(Error::code).collect();
assert_eq!(Error::CODES, from_variants.as_slice());
}
#[test]
fn codes_are_unique_identifiers() {
let mut seen = std::collections::BTreeSet::new();
for code in Error::CODES {
assert!(seen.insert(*code), "duplicate code {code}");
assert!(
!code.is_empty() && code.bytes().all(|b| b.is_ascii_alphanumeric()),
"code {code} must be a non-empty ASCII identifier"
);
}
}
#[test]
fn display_text_of_every_variant() {
let expected = [
"invalid base64url",
"invalid JSON: syntax error",
"invalid header: missing \"alg\"",
"unsupported algorithm",
"algorithm not allowed",
"invalid key: RSA modulus is under 2048 bits",
"key does not match the algorithm",
"signature verification failed",
"decryption failed",
"unknown critical header parameter",
"token expired",
"token not yet valid",
"invalid claim: aud",
"randomness unavailable",
"invalid format: compact JWS must have three parts",
"no matching key",
"several keys match",
];
let actual: Vec<String> = one_of_each().iter().map(Error::to_string).collect();
assert_eq!(actual, expected);
}
const MARKER: &str = "pj-SECRET-marker-7f3a";
fn assert_no_marker(e: &Error) {
let debug = format!("{e:?}");
let display = e.to_string();
assert!(!debug.contains(MARKER), "Debug leaks input: {debug}");
assert!(!display.contains(MARKER), "Display leaks input: {display}");
}
#[test]
fn no_variant_renders_input_or_has_a_cause() {
for e in one_of_each() {
assert_no_marker(&e);
assert!(!e.to_string().is_empty(), "{} has no message", e.code());
assert!(
std::error::Error::source(&e).is_none(),
"{} exposes a cause",
e.code()
);
}
}
#[test]
fn decryption_failed_has_no_detail() {
let e = Error::DecryptionFailed;
assert_eq!(format!("{e:?}"), "DecryptionFailed");
assert_eq!(e.to_string(), "decryption failed");
assert!(std::error::Error::source(&e).is_none());
}
fn marked_malformed_json() -> Vec<(Vec<u8>, &'static str)> {
let mut invalid_utf8 = format!("\"{MARKER}").into_bytes();
invalid_utf8.extend_from_slice(&[0xff, 0xfe, b'"']);
vec![
(
format!("{{\"{MARKER}\" {MARKER}}}").into_bytes(),
"unexpected character",
),
(
format!("{{\"{MARKER}\":").into_bytes(),
"unexpected end of input",
),
(
format!("\"{MARKER}\" {MARKER}").into_bytes(),
"trailing content after the value",
),
(format!("\"{MARKER}\\q\"").into_bytes(), "invalid escape"),
(
format!("\"{MARKER}\\ud800\"").into_bytes(),
"lone surrogate escape",
),
(
format!("\"{MARKER}\n\"").into_bytes(),
"unescaped control character in string",
),
(
format!("{{\"{MARKER}\":1,\"{MARKER}\":2}}").into_bytes(),
"duplicate member name",
),
(
format!("{{\"{MARKER}\":1e400}}").into_bytes(),
"number out of range",
),
(invalid_utf8, "invalid UTF-8"),
]
}
#[test]
fn json_errors_are_invalid_json_without_input() {
for (input, detail) in marked_malformed_json() {
assert!(
input.windows(MARKER.len()).any(|w| w == MARKER.as_bytes()),
"probe for {detail:?} lacks the marker"
);
let e = crate::json::parse(&input).err();
assert_eq!(e, Some(Error::InvalidJson(detail)));
if let Some(e) = e {
assert_eq!(e.code(), "InvalidJson");
assert_no_marker(&e);
assert!(std::error::Error::source(&e).is_none());
}
}
}
}