Skip to main content

sourse_hash/
lib.rs

1//! Domain-separated BLAKE3 hashing for Sourse.
2//!
3//! This crate provides deterministic, domain-separated hashing so that the same
4//! bytes hash differently across contexts (blob, manifest, commit). It contains
5//! no storage or graph logic.
6
7use blake3::Hasher as Blake3Hasher;
8use sourse_core::ObjectId;
9
10/// A 256-bit hash value.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
12pub struct Hash256(pub [u8; 32]);
13
14/// Domain separation labels for different hash contexts.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum HashDomain {
17    /// Blob content hashing.
18    Blob,
19    /// Manifest node hashing.
20    ManifestNode,
21    /// Commit hashing.
22    Commit,
23}
24
25impl HashDomain {
26    /// Returns the domain separation prefix string.
27    pub fn prefix(&self) -> &'static str {
28        match self {
29            Self::Blob => "sourse-blob-v1:",
30            Self::ManifestNode => "sourse-manifest-v1:",
31            Self::Commit => "sourse-commit-v1:",
32        }
33    }
34}
35
36/// A Sourse hasher wrapping BLAKE3 with domain separation.
37pub struct Hasher {
38    inner: Blake3Hasher,
39}
40
41impl Hasher {
42    /// Creates a new hasher for the given domain.
43    pub fn new(domain: HashDomain) -> Self {
44        let mut inner = Blake3Hasher::new();
45        inner.update(domain.prefix().as_bytes());
46        Self { inner }
47    }
48
49    /// Updates the hasher with additional data.
50    pub fn update(&mut self, data: &[u8]) {
51        self.inner.update(data);
52    }
53
54    /// Finalizes and returns the hash.
55    pub fn finalize(&self) -> Hash256 {
56        let hash = self.inner.finalize();
57        Hash256(*hash.as_bytes())
58    }
59}
60
61/// Hashes blob content with domain separation.
62pub fn hash_blob(data: &[u8]) -> Hash256 {
63    let mut h = Hasher::new(HashDomain::Blob);
64    h.update(data);
65    h.finalize()
66}
67
68/// Hashes manifest node content with domain separation.
69pub fn hash_manifest_node(data: &[u8]) -> Hash256 {
70    let mut h = Hasher::new(HashDomain::ManifestNode);
71    h.update(data);
72    h.finalize()
73}
74
75/// Hashes commit content with domain separation.
76pub fn hash_commit(data: &[u8]) -> Hash256 {
77    let mut h = Hasher::new(HashDomain::Commit);
78    h.update(data);
79    h.finalize()
80}
81
82/// Converts a `Hash256` to an `ObjectId`.
83pub fn hash_to_object_id(hash: &Hash256) -> ObjectId {
84    ObjectId(hex::encode(hash.0))
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn domains_produce_distinct_hashes() {
93        let data = b"hello sourse";
94        let blob = hash_blob(data);
95        let manifest = hash_manifest_node(data);
96        let commit = hash_commit(data);
97        assert_ne!(blob, manifest);
98        assert_ne!(blob, commit);
99        assert_ne!(manifest, commit);
100    }
101
102    #[test]
103    fn deterministic_hashing() {
104        let data = b"deterministic test";
105        let h1 = hash_blob(data);
106        let h2 = hash_blob(data);
107        assert_eq!(h1, h2);
108    }
109}