sourse-hash 0.0.1-pre-alpha.0

Domain-separated BLAKE3 hashing for Sourse
Documentation
//! Domain-separated BLAKE3 hashing for Sourse.
//!
//! This crate provides deterministic, domain-separated hashing so that the same
//! bytes hash differently across contexts (blob, manifest, commit). It contains
//! no storage or graph logic.

use blake3::Hasher as Blake3Hasher;
use sourse_core::ObjectId;

/// A 256-bit hash value.
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Hash256(pub [u8; 32]);

/// Domain separation labels for different hash contexts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashDomain {
    /// Blob content hashing.
    Blob,
    /// Manifest node hashing.
    ManifestNode,
    /// Commit hashing.
    Commit,
}

impl HashDomain {
    /// Returns the domain separation prefix string.
    pub fn prefix(&self) -> &'static str {
        match self {
            Self::Blob => "sourse-blob-v1:",
            Self::ManifestNode => "sourse-manifest-v1:",
            Self::Commit => "sourse-commit-v1:",
        }
    }
}

/// A Sourse hasher wrapping BLAKE3 with domain separation.
pub struct Hasher {
    inner: Blake3Hasher,
}

impl Hasher {
    /// Creates a new hasher for the given domain.
    pub fn new(domain: HashDomain) -> Self {
        let mut inner = Blake3Hasher::new();
        inner.update(domain.prefix().as_bytes());
        Self { inner }
    }

    /// Updates the hasher with additional data.
    pub fn update(&mut self, data: &[u8]) {
        self.inner.update(data);
    }

    /// Finalizes and returns the hash.
    pub fn finalize(&self) -> Hash256 {
        let hash = self.inner.finalize();
        Hash256(*hash.as_bytes())
    }
}

/// Hashes blob content with domain separation.
pub fn hash_blob(data: &[u8]) -> Hash256 {
    let mut h = Hasher::new(HashDomain::Blob);
    h.update(data);
    h.finalize()
}

/// Hashes manifest node content with domain separation.
pub fn hash_manifest_node(data: &[u8]) -> Hash256 {
    let mut h = Hasher::new(HashDomain::ManifestNode);
    h.update(data);
    h.finalize()
}

/// Hashes commit content with domain separation.
pub fn hash_commit(data: &[u8]) -> Hash256 {
    let mut h = Hasher::new(HashDomain::Commit);
    h.update(data);
    h.finalize()
}

/// Converts a `Hash256` to an `ObjectId`.
pub fn hash_to_object_id(hash: &Hash256) -> ObjectId {
    ObjectId(hex::encode(hash.0))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn domains_produce_distinct_hashes() {
        let data = b"hello sourse";
        let blob = hash_blob(data);
        let manifest = hash_manifest_node(data);
        let commit = hash_commit(data);
        assert_ne!(blob, manifest);
        assert_ne!(blob, commit);
        assert_ne!(manifest, commit);
    }

    #[test]
    fn deterministic_hashing() {
        let data = b"deterministic test";
        let h1 = hash_blob(data);
        let h2 = hash_blob(data);
        assert_eq!(h1, h2);
    }
}