1use alloc::format;
5use alloc::string::{String, ToString};
6use core::fmt::Write;
7use tiny_keccak::Hasher;
8
9pub(crate) trait ContentHasher {
15 const PREFIX: &'static str;
17
18 type HashOutput: AsRef<[u8]>;
20
21 fn hash_bytes(content: &[u8]) -> Self::HashOutput;
23
24 fn hash(content: &[u8]) -> String {
26 multibase::encode(multibase::Base::Base64Url, Self::hash_bytes(content))
27 }
28}
29
30pub(crate) struct Sha3Cj;
32
33impl ContentHasher for Sha3Cj {
34 const PREFIX: &'static str = "cj";
35 type HashOutput = [u8; 32];
36
37 fn hash_bytes(content: &[u8]) -> [u8; 32] {
38 let mut hasher = tiny_keccak::Sha3::v256();
39 let mut hash_output = [0u8; 32];
40 hasher.update(content);
41 hasher.finalize(&mut hash_output);
42 hash_output
43 }
44}
45
46pub(crate) struct Blake3Cm;
48
49impl ContentHasher for Blake3Cm {
50 const PREFIX: &'static str = "cm";
51 type HashOutput = [u8; 32];
52
53 fn hash_bytes(content: &[u8]) -> [u8; 32] {
54 let hash = blake3::hash(content);
55 *hash.as_bytes()
56 }
57}
58
59#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
66#[cfg_attr(
67 feature = "substrate",
68 derive(
69 parity_scale_codec::Encode,
70 parity_scale_codec::Decode,
71 parity_scale_codec::MaxEncodedLen,
72 parity_scale_codec::DecodeWithMemTracking,
73 scale_info::TypeInfo
74 )
75)]
76pub enum JsonDigest {
77 Sha3([u8; 32]),
79 Blake3([u8; 32]),
81}
82
83impl JsonDigest {
84 pub fn kind_char(self) -> char {
86 match self {
87 Self::Sha3(_) => 'j',
88 Self::Blake3(_) => 'm',
89 }
90 }
91
92 pub fn is_sha3(self) -> bool {
94 matches!(self, Self::Sha3(_))
95 }
96
97 pub fn is_blake3(self) -> bool {
99 matches!(self, Self::Blake3(_))
100 }
101
102 pub fn as_bytes(&self) -> &[u8] {
104 match self {
105 Self::Sha3(b) => b,
106 Self::Blake3(b) => b,
107 }
108 }
109}
110
111impl core::fmt::Display for JsonDigest {
112 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
113 let (kind, bytes): (char, &[u8]) = match self {
114 Self::Sha3(b) => ('j', b),
115 Self::Blake3(b) => ('m', b),
116 };
117 f.write_char('c')?;
118 f.write_char(kind)?;
119 f.write_str(&multibase::encode(multibase::Base::Base64Url, bytes))
120 }
121}
122
123impl core::str::FromStr for JsonDigest {
124 type Err = crate::error::JsonDigestError;
125
126 fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
127 let mut chars = s.chars();
128 if chars.next() != Some('c') {
129 return Err(crate::error::JsonDigestError::Internal(
130 "JsonDigest must start with 'c'".into(),
131 ));
132 }
133 let kind = chars.next().ok_or_else(|| {
134 crate::error::JsonDigestError::Internal("Missing JsonDigest kind character".into())
135 })?;
136 let multibase_str = chars.as_str();
137 let (_base, bytes) = multibase::decode(multibase_str).map_err(|e| {
138 crate::error::JsonDigestError::Internal(format!("Multibase decode error: {e}"))
139 })?;
140 match kind {
141 'j' => {
142 let arr: [u8; 32] = bytes.try_into().map_err(|_| {
143 crate::error::JsonDigestError::Internal(
144 "SHA3 JsonDigest must be 32 bytes".into(),
145 )
146 })?;
147 Ok(Self::Sha3(arr))
148 }
149 'm' => {
150 let arr: [u8; 32] = bytes.try_into().map_err(|_| {
151 crate::error::JsonDigestError::Internal(
152 "BLAKE3 JsonDigest must be 32 bytes".into(),
153 )
154 })?;
155 Ok(Self::Blake3(arr))
156 }
157 _ => Err(crate::error::JsonDigestError::Internal(format!(
158 "Unknown JsonDigest kind '{kind}'"
159 ))),
160 }
161 }
162}
163
164impl serde::Serialize for JsonDigest {
165 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
166 serializer.serialize_str(&self.to_string())
167 }
168}
169
170impl<'de> serde::Deserialize<'de> for JsonDigest {
171 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
172 let s = String::deserialize(deserializer)?;
173 s.parse().map_err(serde::de::Error::custom)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn display_roundtrip_sha3() {
183 let digest = JsonDigest::Sha3([0xABu8; 32]);
184 let s = digest.to_string();
185 assert!(s.starts_with("cj"), "expected cj prefix, got: {s}");
186 let parsed: JsonDigest = s.parse().unwrap();
187 assert_eq!(parsed, digest);
188 }
189
190 #[test]
191 fn display_roundtrip_blake3() {
192 let digest = JsonDigest::Blake3([0xABu8; 32]);
193 let s = digest.to_string();
194 assert!(s.starts_with("cm"), "expected cm prefix, got: {s}");
195 let parsed: JsonDigest = s.parse().unwrap();
196 assert_eq!(parsed, digest);
197 }
198
199 #[test]
200 fn parse_rejects_missing_c() {
201 assert!("jF...".parse::<JsonDigest>().is_err());
202 }
203
204 #[test]
205 fn parse_rejects_unknown_kind() {
206 assert!("cxF...".parse::<JsonDigest>().is_err());
207 }
208
209 #[test]
210 fn parse_rejects_wrong_length() {
211 let short = format!("cj{}", multibase::encode(multibase::Base::Base64Url, [0u8; 16]));
212 assert!(short.parse::<JsonDigest>().is_err());
213 }
214}