1use anyhow::{anyhow, Result};
2use serde::Serialize;
3
4use super::AlgorithmBytes;
5
6const HASH_SEPARATOR: char = '.';
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum HashAlgorithm {
10 B3,
11}
12
13pub fn parse_hash(value: &str) -> Result<AlgorithmBytes<HashAlgorithm>> {
14 let (algorithm_str, value_b58) = value
15 .split_once(HASH_SEPARATOR)
16 .ok_or_else(|| anyhow!("Hash must use '{{algorithm}}.{{base58}}' format"))?;
17 let algorithm = match algorithm_str {
18 "b3" => HashAlgorithm::B3,
19 other => return Err(anyhow!("Unsupported hash algorithm: '{}'", other)),
20 };
21 if value_b58.is_empty() {
22 return Err(anyhow!("Hash payload must not be empty"));
23 }
24
25 let bytes = bs58::decode(value_b58)
26 .into_vec()
27 .map_err(|e| anyhow!("Invalid hash base58 payload: {}", e))?;
28 if bytes.is_empty() {
29 return Err(anyhow!("Hash payload must not decode to empty bytes"));
30 }
31
32 Ok(AlgorithmBytes { algorithm, bytes })
33}
34
35pub fn format_hash(algorithm: HashAlgorithm, bytes: &[u8]) -> String {
36 format!(
37 "{}.{}",
38 algorithm.prefix(),
39 bs58::encode(bytes).into_string()
40 )
41}
42
43pub fn compute_blake3_hash(data: &[u8]) -> String {
45 let hash = blake3::hash(data);
46 format_hash(HashAlgorithm::B3, hash.as_bytes())
47}
48
49pub(crate) fn compute_signed_core_hash<T: Serialize>(
50 core: &T,
51 core_signature: &str,
52) -> Result<String> {
53 let hash_input = serde_json::json!({
54 "core": core,
55 "core_signature": core_signature,
56 });
57 let canonical = serde_jcs::to_string(&hash_input)
58 .map_err(|e| anyhow!("JCS serialization failed: {}", e))?;
59 Ok(compute_blake3_hash(canonical.as_bytes()))
60}
61
62pub(crate) fn compute_tree_signed_core_hash<T: Serialize>(
63 tree_hash: &str,
64 core: &T,
65 core_signature: &str,
66) -> Result<String> {
67 let hash_input = serde_json::json!({
68 "tree_hash": tree_hash,
69 "core": core,
70 "core_signature": core_signature,
71 });
72 let canonical = serde_jcs::to_string(&hash_input)
73 .map_err(|e| anyhow!("JCS serialization failed: {}", e))?;
74 Ok(compute_blake3_hash(canonical.as_bytes()))
75}
76
77impl HashAlgorithm {
78 fn prefix(self) -> &'static str {
79 match self {
80 Self::B3 => "b3",
81 }
82 }
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
87mod tests {
88
89 use super::*;
90
91 #[test]
92 fn test_parse_hash_roundtrip() {
93 let parsed = parse_hash("b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2").unwrap();
94 assert_eq!(parsed.algorithm, HashAlgorithm::B3);
95 let normalized = format_hash(parsed.algorithm, &parsed.bytes);
96 assert_eq!(normalized, "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2");
97 }
98
99 #[test]
100 fn test_parse_hash_rejects_unknown_algorithm() {
101 assert!(parse_hash("sha256.abc").is_err());
102 }
103
104 #[test]
105 fn test_compute_blake3_hash() {
106 let hash = compute_blake3_hash(b"hello");
107 assert!(hash.starts_with("b3."));
108 assert!(hash.len() > 10);
109 assert_eq!(hash, compute_blake3_hash(b"hello"));
110 assert_ne!(hash, compute_blake3_hash(b"world"));
111 }
112
113 #[test]
114 fn test_blake3_hash_for_content_addressing() {
115 let content = r#"{"name":"test","version":"1.0"}"#;
116 let hash1 = compute_blake3_hash(content.as_bytes());
117 let hash2 = compute_blake3_hash(content.as_bytes());
118 assert_eq!(hash1, hash2);
119
120 let different_content = r#"{"name":"test","version":"1.1"}"#;
121 let hash3 = compute_blake3_hash(different_content.as_bytes());
122 assert_ne!(hash1, hash3);
123 }
124
125 #[test]
126 fn test_compute_signed_core_hash_is_deterministic() {
127 let core = serde_json::json!({"name": "cmn.dev", "updated_at_epoch_ms": 1});
128 let signature = "ed25519.test-signature";
129
130 let hash1 = compute_signed_core_hash(&core, signature).unwrap();
131 let hash2 = compute_signed_core_hash(&core, signature).unwrap();
132
133 assert_eq!(hash1, hash2);
134 }
135
136 #[test]
137 fn test_compute_tree_signed_core_hash_changes_with_tree_hash() {
138 let core = serde_json::json!({"name": "cmn-spec"});
139 let signature = "ed25519.test-signature";
140
141 let hash1 = compute_tree_signed_core_hash("b3.tree-a", &core, signature).unwrap();
142 let hash2 = compute_tree_signed_core_hash("b3.tree-b", &core, signature).unwrap();
143
144 assert_ne!(hash1, hash2);
145 }
146}