use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
pub const DIGEST_LEN: usize = 32;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Digest(#[serde(with = "serde_bytes_32")] [u8; DIGEST_LEN]);
impl Digest {
pub fn of(bytes: &[u8]) -> Self {
Self(*blake3::hash(bytes).as_bytes())
}
pub const fn from_bytes(bytes: [u8; DIGEST_LEN]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; DIGEST_LEN] {
&self.0
}
pub fn to_hex(&self) -> String {
use fmt::Write as _;
self.0.iter().fold(String::with_capacity(64), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
pub fn short(&self, n: usize) -> String {
let hex = self.to_hex();
hex[..n.min(hex.len())].to_string()
}
}
#[derive(Debug, thiserror::Error)]
pub enum DigestParseError {
#[error("expected {expected} hex characters, found {found}")]
Length {
expected: usize,
found: usize,
},
#[error("invalid hex character {ch:?} at position {index}")]
Char {
ch: char,
index: usize,
},
}
impl FromStr for Digest {
type Err = DigestParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != DIGEST_LEN * 2 {
return Err(DigestParseError::Length {
expected: DIGEST_LEN * 2,
found: s.len(),
});
}
let mut out = [0u8; DIGEST_LEN];
for (i, byte) in out.iter_mut().enumerate() {
let pair = &s[i * 2..i * 2 + 2];
*byte = u8::from_str_radix(pair, 16).map_err(|_| {
let (index, ch) = pair
.char_indices()
.find(|(_, c)| !c.is_ascii_hexdigit())
.map(|(j, c)| (i * 2 + j, c))
.unwrap_or((i * 2, pair.chars().next().unwrap_or('?')));
DigestParseError::Char { ch, index }
})?;
}
Ok(Self(out))
}
}
impl fmt::Display for Digest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl fmt::Debug for Digest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Digest({}…)", self.short(12))
}
}
mod serde_bytes_32 {
use super::DIGEST_LEN;
use serde::de::{Error as _, Unexpected};
use serde::{Deserialize, Deserializer, Serializer};
pub(super) fn serialize<S: Serializer>(
bytes: &[u8; DIGEST_LEN],
s: S,
) -> Result<S::Ok, S::Error> {
if s.is_human_readable() {
return s.serialize_str(&super::Digest(*bytes).to_hex());
}
s.serialize_bytes(bytes)
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(
d: D,
) -> Result<[u8; DIGEST_LEN], D::Error> {
if d.is_human_readable() {
let hex = String::deserialize(d)?;
return hex
.parse::<super::Digest>()
.map(|digest| *digest.as_bytes())
.map_err(|_| D::Error::invalid_value(Unexpected::Str(&hex), &"64 hex characters"));
}
let raw = serde_bytes_helper::ByteBuf::deserialize(d)?;
<[u8; DIGEST_LEN]>::try_from(raw.0.as_slice())
.map_err(|_| D::Error::invalid_value(Unexpected::Bytes(&raw.0), &"exactly 32 bytes"))
}
mod serde_bytes_helper {
use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
use std::fmt;
pub(super) struct ByteBuf(pub(super) Vec<u8>);
impl<'de> Deserialize<'de> for ByteBuf {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = ByteBuf;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a byte string")
}
fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<ByteBuf, E> {
Ok(ByteBuf(v.to_vec()))
}
fn visit_byte_buf<E: serde::de::Error>(self, v: Vec<u8>) -> Result<ByteBuf, E> {
Ok(ByteBuf(v))
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<ByteBuf, A::Error> {
let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(32));
while let Some(b) = seq.next_element::<u8>()? {
out.push(b);
}
Ok(ByteBuf(out))
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<ByteBuf, E> {
Err(E::custom(format!("expected bytes, found string {v:?}")))
}
}
d.deserialize_bytes(V)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_is_lowercase_and_64_chars() {
let d = Digest::of(b"boundgraph");
let hex = d.to_hex();
assert_eq!(hex.len(), 64);
assert_eq!(hex, hex.to_lowercase());
assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn hex_round_trips() {
let d = Digest::of(b"boundgraph");
assert_eq!(Digest::from_str(&d.to_hex()).unwrap(), d);
}
#[test]
fn the_canonical_encoding_of_a_digest_is_fixed() {
let digest = Digest::of(b"boundgraph");
let encoded = crate::to_canonical_cbor(&digest).unwrap();
assert_eq!(encoded.len(), 34, "a two-byte header and 32 bytes");
assert_eq!(encoded[0], 0x58, "byte string, one-byte length");
assert_eq!(encoded[1], 0x20, "32 bytes long");
assert_eq!(&encoded[2..], digest.as_bytes());
let back: Digest = crate::from_canonical_cbor(&encoded).unwrap();
assert_eq!(back, digest);
}
#[test]
fn a_digest_reads_as_hex_in_a_human_readable_format() {
let digest = Digest::of(b"boundgraph");
let json = serde_json::to_string(&digest).unwrap();
assert_eq!(json, format!("\"{}\"", digest.to_hex()));
assert!(
json.contains(&digest.to_hex()),
"so `grep <digest>` finds the line that mentions it"
);
let back: Digest = serde_json::from_str(&json).unwrap();
assert_eq!(back, digest);
}
#[test]
fn a_malformed_hex_digest_is_refused_rather_than_truncated() {
for bad in ["\"\"", "\"nothex\"", "\"ab\"", "\"zz00\""] {
assert!(
serde_json::from_str::<Digest>(bad).is_err(),
"{bad} must not parse as a digest"
);
}
}
#[test]
fn hashing_is_stable_and_input_sensitive() {
assert_eq!(Digest::of(b"a"), Digest::of(b"a"));
assert_ne!(Digest::of(b"a"), Digest::of(b"b"));
}
#[test]
fn rejects_bad_hex() {
assert!(matches!(
Digest::from_str("abc"),
Err(DigestParseError::Length { .. })
));
let bad = "z".repeat(64);
assert!(matches!(
Digest::from_str(&bad),
Err(DigestParseError::Char { .. })
));
}
#[test]
fn debug_is_abbreviated_but_display_is_not() {
let d = Digest::of(b"boundgraph");
assert!(format!("{d:?}").len() < d.to_string().len());
assert_eq!(d.to_string(), d.to_hex());
}
}