evidence 0.1.0

Type-level tags for cryptographic primitives
Documentation
//! Content-addressed fingerprints (truncated digests).
//!
//! A [`Fingerprint<T, N>`] is a fixed-size content identifier derived from
//! any [`Digest<T, P, C>`](crate::digest::Digest) by taking the first `N`
//! bytes. It is algorithm-agnostic — the same fingerprint type can be
//! constructed from SHA-256, BLAKE3, or any other digest, as long as the
//! digest is at least `N` bytes.
//!
//! Fingerprints are suitable as map keys, content addresses, and compact
//! identifiers for deduplication or lookup.
//!
//! # Example
//!
//! ```
//! # #[cfg(feature = "sha2")]
//! # {
//! use evidence::codec::Identity;
//! use evidence::digest::{Digest, sha2::Sha256};
//! use evidence::fingerprint::Fingerprint;
//! use hybrid_array::typenum::U16;
//!
//! let data = b"hello world";
//! let digest: Digest<[u8; 11], Sha256, Identity> = Digest::hash(data);
//!
//! // Take the first 16 bytes as a fingerprint
//! let fp: Fingerprint<[u8; 11], U16> = Fingerprint::from_digest(&digest);
//! assert_eq!(fp.as_bytes().len(), 16);
//! assert_eq!(fp.as_bytes(), &digest.as_bytes()[..16]);
//! # }
//! ```

use core::marker::PhantomData;

use hybrid_array::{
    Array, ArraySize,
    typenum::{IsLessOrEqual, True},
};

use crate::digest::{Digest, DigestPrimitive};

/// A truncated content-addressed identifier.
///
/// `T` tracks the value type (phantom) and `N` is the byte length.
/// Constructed from any [`Digest<T, P, C>`] whose output is at least
/// `N` bytes via [`from_digest`](Self::from_digest).
pub struct Fingerprint<T, N: ArraySize> {
    bytes: Array<u8, N>,
    _marker: PhantomData<fn() -> T>,
}

impl<T, N: ArraySize> Clone for Fingerprint<T, N>
where
    Array<u8, N>: Clone,
{
    fn clone(&self) -> Self {
        Self {
            bytes: self.bytes.clone(),
            _marker: PhantomData,
        }
    }
}

impl<T, N: ArraySize> Copy for Fingerprint<T, N> where Array<u8, N>: Copy {}

impl<T, N: ArraySize> PartialEq for Fingerprint<T, N>
where
    Array<u8, N>: PartialEq,
{
    fn eq(&self, other: &Self) -> bool {
        self.bytes == other.bytes
    }
}

impl<T, N: ArraySize> Eq for Fingerprint<T, N> where Array<u8, N>: Eq {}

impl<T, N: ArraySize> PartialOrd for Fingerprint<T, N>
where
    Array<u8, N>: PartialOrd,
{
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl<T, N: ArraySize> Ord for Fingerprint<T, N>
where
    Array<u8, N>: Ord,
{
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        self.bytes.cmp(&other.bytes)
    }
}

impl<T, N: ArraySize> core::hash::Hash for Fingerprint<T, N>
where
    Array<u8, N>: core::hash::Hash,
{
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        self.bytes.hash(state);
    }
}

impl<T, N: ArraySize> core::fmt::Debug for Fingerprint<T, N> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "Fingerprint({:02x?})", self.bytes.as_slice())
    }
}

impl<T, N: ArraySize> Fingerprint<T, N> {
    /// Internal constructor.
    #[must_use]
    pub(crate) const fn new(bytes: Array<u8, N>) -> Self {
        Self {
            bytes,
            _marker: PhantomData,
        }
    }

    /// Create a fingerprint by truncating a digest to `N` bytes.
    ///
    /// The digest must be at least `N` bytes long, which is enforced
    /// at compile time via typenum bounds.
    ///
    /// # Panics
    ///
    /// Cannot panic in practice — the `N <= P::Size` bound is enforced at
    /// compile time. The internal `expect` exists only because the slice-to-array
    /// conversion is checked at runtime by `hybrid_array`.
    #[must_use]
    #[allow(clippy::expect_used, clippy::indexing_slicing)] // N <= P::Size enforced by typenum bound
    pub fn from_digest<P: DigestPrimitive, C>(digest: &Digest<T, P, C>) -> Self
    where
        N: IsLessOrEqual<P::Size, Output = True>,
    {
        let bytes = Array::try_from(&digest.as_bytes()[..N::USIZE])
            .expect("N <= P::Size is enforced by type bound");
        Self::new(bytes)
    }

    /// Get the fingerprint as a byte array reference.
    #[must_use]
    pub const fn as_array(&self) -> &Array<u8, N> {
        &self.bytes
    }

    /// Get the fingerprint as a byte slice.
    #[must_use]
    pub const fn as_bytes(&self) -> &[u8] {
        self.bytes.as_slice()
    }

    /// Consume the fingerprint and return the underlying byte array.
    #[must_use]
    pub fn into_array(self) -> Array<u8, N> {
        self.bytes
    }
}

/// Extension trait for constructing fingerprints 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
/// [`Fingerprint`] normally provides.
///
/// # When to use
///
/// - Deserializing a fingerprint from storage or network
/// - Interoperating with external systems that provide raw identifier bytes
/// - Testing
///
/// # Example
///
/// ```
/// use evidence::fingerprint::{Fingerprint, FingerprintUnchecked};
/// use hybrid_array::{typenum::U16, Array};
///
/// let bytes: Array<u8, U16> = Array::try_from([0u8; 16].as_slice()).unwrap();
/// let fp: Fingerprint<String, U16> = Fingerprint::from_unchecked_array(bytes);
/// ```
pub trait FingerprintUnchecked<T, N: ArraySize> {
    /// Create a fingerprint from a raw byte array.
    ///
    /// # Safety (logical)
    ///
    /// This does not perform any hashing or truncation. The caller must
    /// ensure the bytes represent a valid fingerprint of a `T` value.
    /// Prefer [`Fingerprint::from_digest`] when possible.
    #[must_use]
    fn from_unchecked_array(bytes: Array<u8, N>) -> Self;
}

impl<T, N: ArraySize> FingerprintUnchecked<T, N> for Fingerprint<T, N> {
    fn from_unchecked_array(bytes: Array<u8, N>) -> Self {
        Self::new(bytes)
    }
}

#[cfg(feature = "serde")]
impl<T, N: ArraySize> serde::Serialize for Fingerprint<T, N>
where
    Array<u8, N>: 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, N: ArraySize> serde::Deserialize<'de> for Fingerprint<T, N>
where
    Array<u8, N>: 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, N: ArraySize> arbitrary::Arbitrary<'a> for Fingerprint<T, N>
where
    Array<u8, N>: 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, N: ArraySize + 'static> bolero_generator::TypeGenerator for Fingerprint<T, N>
where
    Array<u8, N>: 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, N: ArraySize + 'static> proptest::arbitrary::Arbitrary for Fingerprint<T, N>
where
    Array<u8, N>: 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 proptest::prelude::*;
        proptest::collection::vec(any::<u8>(), N::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 [`Fingerprint`].
pub mod archive {
    use super::{Array, ArraySize, Fingerprint};
    use alloc::vec::Vec;
    use rkyv::{Archive, Deserialize, Serialize, rancor::Fallible};

    /// Helper struct for rkyv serialization.
    ///
    /// The phantom type parameters from [`Fingerprint`] are erased in the
    /// archived form since they only matter at compile time.
    #[derive(Debug, Archive, Serialize, Deserialize)]
    #[rkyv(derive(Debug))]
    pub struct FingerprintBytes {
        bytes: Vec<u8>,
    }

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

    impl<T, N: ArraySize> Archive for Fingerprint<T, N> {
        type Archived = ArchivedFingerprintBytes;
        type Resolver = <FingerprintBytes as Archive>::Resolver;

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

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

    impl<T, N: ArraySize, D> Deserialize<Fingerprint<T, N>, D> for ArchivedFingerprintBytes
    where
        D: Fallible + ?Sized,
        D::Error: rkyv::rancor::Source,
    {
        #[allow(clippy::expect_used)] // length validated during serialization
        fn deserialize(&self, deserializer: &mut D) -> Result<Fingerprint<T, N>, D::Error> {
            let helper: FingerprintBytes = <ArchivedFingerprintBytes as Deserialize<
                FingerprintBytes,
                D,
            >>::deserialize(self, deserializer)?;
            let bytes =
                Array::try_from(helper.bytes.as_slice()).expect("invalid fingerprint length");
            Ok(Fingerprint::new(bytes))
        }
    }
}