#[cfg_attr(
feature = "wire",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
#[cfg_attr(feature = "wire", rkyv(compare(PartialEq), derive(Debug)))]
#[derive(Clone, Copy, PartialOrd, Ord)]
pub struct Digest([u8; 32]);
impl std::hash::Hash for Digest {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl PartialEq for Digest {
fn eq(&self, other: &Self) -> bool {
blake3::Hash::from_bytes(self.0) == blake3::Hash::from_bytes(other.0)
}
}
impl Eq for Digest {}
impl Digest {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub fn ct_eq(&self, other: &Self) -> bool {
self == other
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn to_hex(&self) -> String {
crate::hex::encode(self.0)
}
pub fn from_hex(text: &str) -> Result<Self, DigestParseError> {
if text.len() != 64 {
let refusal = Err(DigestParseError::WrongLength { len: text.len() });
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "from_hex: returning an error to the caller");
return refusal;
}
let mut raw = [0; 32];
crate::hex::decode_into(text, &mut raw).map_err(DigestParseError::Hex)?;
Ok(Self(raw))
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DigestParseError {
WrongLength {
len: usize,
},
Hex(crate::hex::DecodeError),
}
impl core::fmt::Display for DigestParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match *self {
Self::WrongLength { len } => {
write!(f, "digest hex must be 64 characters, got {len}")
}
Self::Hex(ref err) => write!(f, "{err}"),
}
}
}
impl std::error::Error for DigestParseError {}
impl core::fmt::Debug for Digest {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("Digest(")?;
crate::hex::write_lowercase(&self.0, f)?;
f.write_str(")")
}
}
impl core::fmt::Display for Digest {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
crate::hex::write_lowercase(&self.0, f)
}
}
#[must_use]
pub fn blake3(data: &[u8]) -> Digest {
Digest(*blake3::hash(data).as_bytes())
}
#[must_use]
pub fn keyed(key: &[u8; 32], data: &[u8]) -> Digest {
Digest(*blake3::keyed_hash(key, data).as_bytes())
}
pub struct Hasher(blake3::Hasher);
impl core::fmt::Debug for Hasher {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Hasher")
.field("bytes_written", &self.0.count())
.finish_non_exhaustive()
}
}
impl Hasher {
#[must_use]
pub fn new() -> Self {
Self(blake3::Hasher::new())
}
pub fn update(&mut self, data: &[u8]) -> &mut Self {
self.0.update(data);
self
}
pub fn write_framed(&mut self, data: &[u8]) -> &mut Self {
let len = u64::try_from(data.len()).unwrap_or(u64::MAX);
self.0.update(&len.to_le_bytes());
self.0.update(data);
self
}
#[must_use]
pub fn finalize(&self) -> Digest {
Digest(*self.0.finalize().as_bytes())
}
}
impl Default for Hasher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_matches_blake3_spec() {
let digest = blake3(b"");
assert_eq!(
digest.to_hex(),
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
);
}
#[test]
fn deterministic_across_calls() {
let first = blake3(b"hello world");
let second = blake3(b"hello world");
assert_eq!(first, second);
}
#[test]
fn different_input_different_digest() {
assert_ne!(blake3(b"a"), blake3(b"b"));
}
#[test]
fn incremental_matches_oneshot() {
let oneshot = blake3(b"hello world");
let mut hasher = Hasher::new();
hasher.update(b"hello ");
hasher.update(b"world");
assert_eq!(hasher.finalize(), oneshot);
}
#[test]
fn framing_separates_splits_concatenation_conflates() {
let mut unframed_left = Hasher::new();
unframed_left.update(b"ab").update(b"c");
let mut unframed_right = Hasher::new();
unframed_right.update(b"a").update(b"bc");
assert_eq!(unframed_left.finalize(), unframed_right.finalize());
let mut framed_left = Hasher::new();
framed_left.write_framed(b"ab").write_framed(b"c");
let mut framed_right = Hasher::new();
framed_right.write_framed(b"a").write_framed(b"bc");
assert_ne!(framed_left.finalize(), framed_right.finalize());
let mut repeat = Hasher::new();
repeat.write_framed(b"ab").write_framed(b"c");
assert_eq!(framed_left.finalize(), repeat.finalize());
}
#[test]
fn equality_sees_each_of_the_32_bytes() {
let base = blake3(b"position");
assert!(base.ct_eq(&Digest::from_bytes(*base.as_bytes())));
for position in 0..32 {
for mask in [0x01_u8, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80] {
let mut bytes = *base.as_bytes();
if let Some(byte) = bytes.get_mut(position) {
*byte ^= mask;
}
let flipped = Digest::from_bytes(bytes);
assert!(
base != flipped && !base.ct_eq(&flipped) && !flipped.ct_eq(&base),
"a flip of mask {mask:#04x} at byte {position} compared equal"
);
}
}
}
#[test]
fn hex_roundtrip() -> Result<(), DigestParseError> {
let digest = blake3(b"test");
let hex = digest.to_hex();
let parsed = Digest::from_hex(&hex)?;
assert_eq!(digest, parsed);
Ok(())
}
#[test]
fn display_is_hex() {
let digest = blake3(b"");
assert_eq!(format!("{digest}"), digest.to_hex());
}
#[test]
fn from_hex_rejects_wrong_length() {
assert!(Digest::from_hex("abcd").is_err());
}
#[test]
fn keyed_differs_from_unkeyed() {
let key = [0x42u8; 32];
assert_ne!(keyed(&key, b"hello world"), blake3(b"hello world"));
}
#[test]
fn keyed_is_key_sensitive() {
assert_ne!(keyed(&[0x01u8; 32], b"data"), keyed(&[0x02u8; 32], b"data"));
}
#[test]
fn keyed_deterministic_across_calls() {
let key = [0x07u8; 32];
assert_eq!(keyed(&key, b"receipt"), keyed(&key, b"receipt"));
}
#[test]
fn keyed_matches_blake3_keyed_hash() {
let key = [0xABu8; 32];
let expected = *blake3::keyed_hash(&key, b"vector").as_bytes();
assert_eq!(keyed(&key, b"vector").as_bytes(), &expected);
}
#[cfg(feature = "wire")]
#[test]
fn archives_and_reads_back_in_place() -> Result<(), crate::wire::WireError> {
let digest = blake3(b"receipt");
let bytes = crate::wire::to_bytes::<crate::wire::WireError>(&digest)?;
assert_eq!(
&digest,
crate::wire::access::<ArchivedDigest, crate::wire::WireError>(&bytes)?,
"the archived form compares equal to the digest it was made from"
);
let other = crate::wire::to_bytes::<crate::wire::WireError>(&blake3(b"other"))?;
assert_ne!(
&digest,
crate::wire::access::<ArchivedDigest, crate::wire::WireError>(&other)?,
"and a different digest does not"
);
Ok(())
}
}