//! Content-addressed digests with phantom type tracking. //! //! [`Digest`] is a hash digest that tracks at the type level: //! - `T`: the type of value that was hashed //! - `P`: the hash primitive used (e.g., [`Sha256`](sha2::Sha256)) //! - `C`: the codec used to serialize the value before hashing //! //! # Example //! //! ``` //! # #[cfg(feature = "sha2")] //! # { //! use evidence::digest::{Digest, sha2::Sha256}; //! use evidence::codec::Identity; //! //! let data = b"hello world"; //! let digest: Digest<[u8; 11], Sha256, Identity> = Digest::hash(data); //! //! assert_eq!(digest.as_bytes().len(), 32); //! # } //! ``` use core::marker::PhantomData; use hybrid_array::{Array, ArraySize}; use crate::codec::Encode; #[cfg(feature = "blake3")] pub mod blake3; #[cfg(feature = "sha2")] pub mod sha2; #[cfg(feature = "sha3")] pub mod sha3; /// A digest with phantom types tracking what was digested and how. pub struct Digest { bytes: Array, _marker: PhantomData (T, C)>, } impl Clone for Digest where Array: Clone, { fn clone(&self) -> Self { Self { bytes: self.bytes.clone(), _marker: PhantomData, } } } impl PartialEq for Digest where Array: PartialEq, { fn eq(&self, other: &Self) -> bool { self.bytes == other.bytes } } impl Eq for Digest where Array: Eq {} impl core::hash::Hash for Digest where Array: core::hash::Hash, { fn hash(&self, state: &mut H) { self.bytes.hash(state); } } impl Digest { /// Compute a digest of the value. /// /// The value is first encoded using codec `C`, then hashed with primitive `P`. /// /// # Panics /// /// Panics if the codec fails to encode the value. #[allow(clippy::expect_used)] // documented panic on encode failure pub fn hash(value: &T) -> Self where C: Encode, { let encoded = C::encode(value).expect("encoding failed"); let bytes = P::hash(&encoded); Self::new(bytes) } /// Internal constructor for creating a digest from raw bytes. #[must_use] pub(crate) const fn new(bytes: Array) -> Self { Self { bytes, _marker: PhantomData, } } /// Get the raw bytes of the digest. #[must_use] pub const fn as_array(&self) -> &Array { &self.bytes } /// Get the digest as a byte slice. #[must_use] pub const fn as_bytes(&self) -> &[u8] { self.bytes.as_slice() } /// Consume the digest and return the underlying byte array. #[must_use] pub fn into_array(self) -> Array { self.bytes } } /// Extension trait for constructing digests from raw bytes. /// /// This trait is _intentionally_ not in the prelude. Importing it is an explicit /// acknowledgment that you are bypassing the type-level guarantees that [`Digest`] /// normally provides. /// /// # When to use /// /// - Deserializing a digest from storage or network /// - Interoperating with external systems that provide raw hash bytes /// - Testing /// /// # Example /// /// ``` /// # #[cfg(feature = "sha2")] /// # { /// use evidence::digest::{Digest, DigestUnchecked, sha2::Sha256}; /// use evidence::codec::Identity; /// use hybrid_array::Array; /// /// // Explicit import required — this is the "escape hatch" /// let bytes: Array = Array::try_from([0u8; 32].as_slice()).unwrap(); /// let digest: Digest = Digest::from_unchecked_array(bytes); /// # } /// ``` pub trait DigestUnchecked { /// Create a digest from a raw byte array. /// /// # Safety (logical) /// /// This does not perform any hashing. The caller must ensure the bytes /// represent a valid digest of a `T` value encoded with codec `C`. /// Prefer [`Digest::hash`] when possible. #[must_use] fn from_unchecked_array(bytes: Array) -> Self; } impl DigestUnchecked for Digest { fn from_unchecked_array(bytes: Array) -> Self { Self::new(bytes) } } impl Copy for Digest where Array: Copy {} impl core::fmt::Debug for Digest { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "Digest({:02x?})", self.bytes.as_slice()) } } /// A hash primitive that can digest bytes into a fixed-size output. pub trait DigestPrimitive { /// The output size of this hash function. type Size: ArraySize; /// Hash the input bytes and return the digest. fn hash(data: &[u8]) -> Array; } #[cfg(feature = "serde")] impl serde::Serialize for Digest where Array: serde::Serialize, { fn serialize(&self, serializer: S) -> Result { self.bytes.serialize(serializer) } } #[cfg(feature = "serde")] impl<'de, T, P: DigestPrimitive, C> serde::Deserialize<'de> for Digest where Array: serde::Deserialize<'de>, { fn deserialize>(deserializer: D) -> Result { let bytes = Array::deserialize(deserializer)?; Ok(Self::new(bytes)) } } #[cfg(feature = "arbitrary")] impl<'a, T, P: DigestPrimitive, C> arbitrary::Arbitrary<'a> for Digest where Array: arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { let bytes = Array::arbitrary(u)?; Ok(Self::new(bytes)) } } #[cfg(feature = "bolero")] impl bolero_generator::TypeGenerator for Digest where Array: bolero_generator::TypeGenerator, { fn generate(driver: &mut D) -> Option { let bytes = Array::generate(driver)?; Some(Self::new(bytes)) } } #[cfg(feature = "proptest")] impl proptest::arbitrary::Arbitrary for Digest where Array: core::fmt::Debug, { type Parameters = (); type Strategy = proptest::strategy::BoxedStrategy; #[allow(clippy::expect_used)] // length is guaranteed by proptest generator fn arbitrary_with((): Self::Parameters) -> Self::Strategy { use hybrid_array::typenum::Unsigned; use proptest::prelude::*; proptest::collection::vec(any::(), P::Size::USIZE) .prop_map(|v| Self::new(Array::try_from(v.as_slice()).expect("correct length"))) .boxed() } } #[cfg(feature = "rkyv")] /// Zero-copy [`rkyv`] serialization support for [`Digest`]. pub mod archive { use super::{Array, Digest, DigestPrimitive}; use alloc::vec::Vec; use rkyv::{Archive, Deserialize, Serialize, rancor::Fallible}; /// Helper struct for rkyv serialization. /// /// The phantom type parameters from [`Digest`] are erased in the archived /// form since they only matter at compile time. #[derive(Debug, Archive, Serialize, Deserialize)] #[rkyv(derive(Debug))] pub struct DigestBytes { bytes: Vec, } impl ArchivedDigestBytes { /// Get the archived bytes as a slice. #[must_use] pub fn as_bytes(&self) -> &[u8] { &self.bytes } } impl Archive for Digest { type Archived = ArchivedDigestBytes; type Resolver = ::Resolver; fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place) { let helper = DigestBytes { bytes: self.bytes.as_slice().to_vec(), }; helper.resolve(resolver, out); } } impl Serialize for Digest where S: Fallible + rkyv::ser::Allocator + rkyv::ser::Writer + ?Sized, { fn serialize(&self, serializer: &mut S) -> Result { let helper = DigestBytes { bytes: self.bytes.as_slice().to_vec(), }; helper.serialize(serializer) } } impl Deserialize, D> for ArchivedDigestBytes where D: Fallible + ?Sized, D::Error: rkyv::rancor::Source, { #[allow(clippy::expect_used)] // length validated during serialization fn deserialize(&self, deserializer: &mut D) -> Result, D::Error> { let helper: DigestBytes = >::deserialize( self, deserializer, )?; let bytes = Array::try_from(helper.bytes.as_slice()).expect("invalid digest length"); Ok(Digest::new(bytes)) } } }