jmap_cid_types/blobext.rs
1//! Base64 conversion for [`Sha256`], for comparison with the RFC 9404
2//! (JMAP Blob Management Extensions) `digest:sha-256` property.
3//!
4//! draft-atwood-jmap-cid-00 §Relationship to JMAP Blob Management
5//! Extensions says the CID `sha256` field "uses lowercase hex encoding
6//! rather than base64", and that clients may cross-validate the two
7//! "provided they convert encodings before comparing". RFC 9404 §4.1
8//! defines `digest:<algorithm>` as "The base64 encoding of the digest",
9//! with base64 as specified in RFC 4648: the §4 alphabet with `=`
10//! padding. A 32-byte SHA-256 digest is always 44 base64 characters.
11//!
12//! The reverse path accepts only canonical padded base64. Missing
13//! padding, the §5 URL-safe alphabet, embedded whitespace and non-zero
14//! trailing bits are all rejected with [`Sha256Base64Error`].
15
16use crate::Sha256;
17use base64::Engine as _;
18use base64::engine::general_purpose::STANDARD;
19use std::fmt;
20
21/// Error from [`Sha256::from_base64`].
22///
23/// `#[non_exhaustive]` at the type level and per variant, so new
24/// variants and new fields stay semver-additive.
25#[non_exhaustive]
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Sha256Base64Error {
28 /// The string is not canonical padded RFC 4648 §4 base64.
29 #[non_exhaustive]
30 InvalidBase64,
31 /// The string decodes to something other than 32 bytes.
32 #[non_exhaustive]
33 WrongLength {
34 /// The decoded byte length, saturated at `u32::MAX`.
35 got: u32,
36 },
37}
38
39impl fmt::Display for Sha256Base64Error {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::InvalidBase64 => f.write_str("SHA-256 digest is not canonical padded base64"),
43 Self::WrongLength { got } => {
44 write!(
45 f,
46 "base64 SHA-256 digest must decode to 32 bytes (got {got})"
47 )
48 }
49 }
50 }
51}
52
53impl std::error::Error for Sha256Base64Error {}
54
55impl Sha256 {
56 /// The digest as padded RFC 4648 §4 base64, the encoding of the
57 /// RFC 9404 `digest:sha-256` property.
58 pub fn to_base64(&self) -> String {
59 STANDARD.encode(self.to_bytes())
60 }
61
62 /// Parse a padded RFC 4648 §4 base64 digest, such as an RFC 9404
63 /// `digest:sha-256` value, into its [`Sha256`].
64 pub fn from_base64(s: &str) -> Result<Self, Sha256Base64Error> {
65 let bytes = STANDARD
66 .decode(s)
67 .map_err(|_| Sha256Base64Error::InvalidBase64)?;
68 let digest: [u8; 32] =
69 bytes
70 .as_slice()
71 .try_into()
72 .map_err(|_| Sha256Base64Error::WrongLength {
73 got: u32::try_from(bytes.len()).unwrap_or(u32::MAX),
74 })?;
75 Ok(Self::from(digest))
76 }
77}