evidence 0.1.0

Type-level tags for cryptographic primitives
Documentation
//! Content-addressed digests with phantom type tracking.
//!
//! [`Digest<T, P, C>`] 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<T, P: DigestPrimitive, C> {
    bytes: Array<u8, P::Size>,
    _marker: PhantomData<fn() -> (T, C)>,
}

impl<T, P: DigestPrimitive, C> Clone for Digest<T, P, C>
where
    Array<u8, P::Size>: Clone,
{
    fn clone(&self) -> Self {
        Self {
            bytes: self.bytes.clone(),
            _marker: PhantomData,
        }
    }
}

impl<T, P: DigestPrimitive, C> PartialEq for Digest<T, P, C>
where
    Array<u8, P::Size>: PartialEq,
{
    fn eq(&self, other: &Self) -> bool {
        self.bytes == other.bytes
    }
}

impl<T, P: DigestPrimitive, C> Eq for Digest<T, P, C> where Array<u8, P::Size>: Eq {}

impl<T, P: DigestPrimitive, C> core::hash::Hash for Digest<T, P, C>
where
    Array<u8, P::Size>: core::hash::Hash,
{
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        self.bytes.hash(state);
    }
}

impl<T, P: DigestPrimitive, C> Digest<T, P, C> {
    /// 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<T>,
    {
        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<u8, P::Size>) -> Self {
        Self {
            bytes,
            _marker: PhantomData,
        }
    }

    /// Get the raw bytes of the digest.
    #[must_use]
    pub const fn as_array(&self) -> &Array<u8, P::Size> {
        &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<u8, P::Size> {
        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<u8, _> = Array::try_from([0u8; 32].as_slice()).unwrap();
/// let digest: Digest<String, Sha256, Identity> = Digest::from_unchecked_array(bytes);
/// # }
/// ```
pub trait DigestUnchecked<T, P: DigestPrimitive, C> {
    /// 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<u8, P::Size>) -> Self;
}

impl<T, P: DigestPrimitive, C> DigestUnchecked<T, P, C> for Digest<T, P, C> {
    fn from_unchecked_array(bytes: Array<u8, P::Size>) -> Self {
        Self::new(bytes)
    }
}

impl<T, P: DigestPrimitive, C> Copy for Digest<T, P, C> where Array<u8, P::Size>: Copy {}

impl<T, P: DigestPrimitive, C> core::fmt::Debug for Digest<T, P, C> {
    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<u8, Self::Size>;
}

#[cfg(feature = "serde")]
impl<T, P: DigestPrimitive, C> serde::Serialize for Digest<T, P, C>
where
    Array<u8, P::Size>: serde::Serialize,
{
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.bytes.serialize(serializer)
    }
}

#[cfg(feature = "serde")]
impl<'de, T, P: DigestPrimitive, C> serde::Deserialize<'de> for Digest<T, P, C>
where
    Array<u8, P::Size>: serde::Deserialize<'de>,
{
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let bytes = Array::deserialize(deserializer)?;
        Ok(Self::new(bytes))
    }
}

#[cfg(feature = "arbitrary")]
impl<'a, T, P: DigestPrimitive, C> arbitrary::Arbitrary<'a> for Digest<T, P, C>
where
    Array<u8, P::Size>: arbitrary::Arbitrary<'a>,
{
    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
        let bytes = Array::arbitrary(u)?;
        Ok(Self::new(bytes))
    }
}

#[cfg(feature = "bolero")]
impl<T: 'static, P: DigestPrimitive + 'static, C: 'static> bolero_generator::TypeGenerator
    for Digest<T, P, C>
where
    Array<u8, P::Size>: bolero_generator::TypeGenerator,
{
    fn generate<D: bolero_generator::Driver>(driver: &mut D) -> Option<Self> {
        let bytes = Array::generate(driver)?;
        Some(Self::new(bytes))
    }
}

#[cfg(feature = "proptest")]
impl<T: 'static, P: DigestPrimitive + 'static, C: 'static> proptest::arbitrary::Arbitrary
    for Digest<T, P, C>
where
    Array<u8, P::Size>: core::fmt::Debug,
{
    type Parameters = ();
    type Strategy = proptest::strategy::BoxedStrategy<Self>;

    #[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::<u8>(), 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<u8>,
    }

    impl ArchivedDigestBytes {
        /// Get the archived bytes as a slice.
        #[must_use]
        pub fn as_bytes(&self) -> &[u8] {
            &self.bytes
        }
    }

    impl<T, P: DigestPrimitive, C> Archive for Digest<T, P, C> {
        type Archived = ArchivedDigestBytes;
        type Resolver = <DigestBytes as Archive>::Resolver;

        fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place<Self::Archived>) {
            let helper = DigestBytes {
                bytes: self.bytes.as_slice().to_vec(),
            };
            helper.resolve(resolver, out);
        }
    }

    impl<T, P: DigestPrimitive, C, S> Serialize<S> for Digest<T, P, C>
    where
        S: Fallible + rkyv::ser::Allocator + rkyv::ser::Writer + ?Sized,
    {
        fn serialize(&self, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
            let helper = DigestBytes {
                bytes: self.bytes.as_slice().to_vec(),
            };
            helper.serialize(serializer)
        }
    }

    impl<T, P: DigestPrimitive, C, D> Deserialize<Digest<T, P, C>, 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<Digest<T, P, C>, D::Error> {
            let helper: DigestBytes =
                <ArchivedDigestBytes as Deserialize<DigestBytes, D>>::deserialize(
                    self,
                    deserializer,
                )?;
            let bytes = Array::try_from(helper.bytes.as_slice()).expect("invalid digest length");
            Ok(Digest::new(bytes))
        }
    }
}