1mod error;
2mod implementations;
3mod methods;
4pub use error::*;
5use ps_base64::{base64, sized_encode};
6use ps_buffer::Buffer;
7use ps_ecc::ReedSolomon;
8use ps_pint16::PackedInt;
9use sha2::{Digest, Sha256};
10use std::fmt::Write;
11
12#[cfg(test)]
13pub mod tests;
14
15pub const HASH_SIZE_BIN: usize = 32;
16pub const HASH_SIZE: usize = 64;
17pub const HASH_SIZE_TOTAL_BIN: usize = 48;
18pub const PARITY: u8 = 7;
19pub const PARITY_OFFSET: usize = 34;
20pub const PARITY_SIZE: usize = 14;
21pub const SIZE_SIZE: usize = std::mem::size_of::<u16>();
22
23pub const RS: ReedSolomon = match ReedSolomon::new(PARITY) {
24 Ok(rs) => rs,
25 Err(_) => panic!("Failed to construct Reed-Solomon codec."),
26};
27
28#[inline]
29#[must_use]
30pub fn sha256(data: &[u8]) -> [u8; HASH_SIZE_BIN] {
31 let mut hasher = Sha256::new();
32
33 hasher.update(data);
34
35 let result = hasher.finalize();
36
37 result.into()
38}
39
40#[inline]
41#[must_use]
42pub fn blake3(data: &[u8]) -> blake3::Hash {
43 blake3::hash(data)
44}
45
46pub type HashParts = ([u8; HASH_SIZE_BIN], [u8; PARITY_SIZE], PackedInt);
47
48#[derive(Clone, Copy, Eq)]
50#[repr(transparent)]
51pub struct Hash {
52 inner: [u8; HASH_SIZE],
53}
54
55impl Hash {
56 #[allow(clippy::self_named_constructors)]
63 pub fn hash<T: AsRef<[u8]>>(data: T) -> Result<Self, HashError> {
64 let data = data.as_ref();
65 let mut buffer = Buffer::with_capacity(HASH_SIZE)?;
66
67 buffer.extend_from_slice(sha256(data))?;
68 buffer ^= &blake3(data).as_bytes()[..];
69 buffer.extend_from_slice(PackedInt::from_usize(data.len()).to_16_bits())?;
70 buffer.extend_from_slice(RS.generate_parity(&buffer)?)?;
71
72 let hash = Self {
73 inner: sized_encode::<HASH_SIZE>(&buffer),
74 };
75
76 Ok(hash)
77 }
78
79 pub fn validate<T: AsRef<[u8]>>(hash: T) -> Result<Self, HashValidationError> {
85 let mut hash = base64::decode(hash.as_ref());
86
87 hash.resize(HASH_SIZE_TOTAL_BIN, 0xF4);
90
91 let (data, parity) = hash.split_at_mut(PARITY_OFFSET);
92
93 ReedSolomon::correct_detached_in_place(parity, data)?;
94
95 let hash = Self {
96 inner: sized_encode::<HASH_SIZE>(&hash),
97 };
98
99 Ok(hash)
100 }
101}
102
103impl AsRef<[u8]> for Hash {
104 fn as_ref(&self) -> &[u8] {
105 self.as_bytes()
106 }
107}
108
109impl std::fmt::Display for Hash {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.write_str(self.as_str())
112 }
113}
114
115impl std::fmt::Debug for Hash {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 for &b in &self.inner {
118 if b.is_ascii_graphic() {
119 f.write_char(b as char)
120 } else {
121 f.write_str(&format!("<0x{b:02X?}>"))
122 }?;
123 }
124
125 Ok(())
126 }
127}
128
129impl core::hash::Hash for Hash {
130 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
131 match decode_parts(&self.inner) {
132 Ok((hash, checksum, length)) => {
133 state.write(&hash);
134 state.write(&checksum);
135 state.write_u16(length.to_inner_u16());
136 }
137 Err(_) => state.write(&self.inner),
138 }
139 }
140}
141
142impl std::ops::Deref for Hash {
143 type Target = str;
144
145 fn deref(&self) -> &Self::Target {
146 self.as_str()
147 }
148}
149
150impl std::ops::Index<usize> for Hash {
151 type Output = u8;
152
153 fn index(&self, index: usize) -> &Self::Output {
154 if index < self.inner.len() {
155 &self.inner[index]
156 } else {
157 &0
158 }
159 }
160}
161
162impl std::ops::Index<std::ops::Range<usize>> for Hash {
163 type Output = str;
164
165 fn index(&self, index: std::ops::Range<usize>) -> &Self::Output {
166 let start = std::cmp::min(index.start, self.inner.len());
167 let end = std::cmp::min(index.end, self.inner.len());
168 let range = start..end;
169
170 &self.as_str()[range]
171 }
172}
173
174impl std::ops::Index<std::ops::RangeFrom<usize>> for Hash {
175 type Output = str;
176
177 fn index(&self, index: std::ops::RangeFrom<usize>) -> &Self::Output {
178 self.index(index.start..HASH_SIZE)
179 }
180}
181
182impl std::ops::Index<std::ops::RangeTo<usize>> for Hash {
183 type Output = str;
184
185 fn index(&self, index: std::ops::RangeTo<usize>) -> &Self::Output {
186 self.index(0..index.end)
187 }
188}
189
190impl std::ops::Index<std::ops::RangeToInclusive<usize>> for Hash {
191 type Output = str;
192
193 fn index(&self, index: std::ops::RangeToInclusive<usize>) -> &Self::Output {
194 &self.as_str()[index]
195 }
196}
197
198impl std::ops::Index<std::ops::RangeFull> for Hash {
199 type Output = str;
200
201 fn index(&self, _: std::ops::RangeFull) -> &Self::Output {
202 return self.as_str();
203 }
204}
205
206impl std::ops::Index<std::ops::RangeInclusive<usize>> for Hash {
207 type Output = str;
208
209 fn index(&self, index: std::ops::RangeInclusive<usize>) -> &Self::Output {
210 &self.as_str()[index]
211 }
212}
213
214impl PartialEq for Hash {
215 fn eq(&self, other: &Self) -> bool {
216 let Ok(left) = decode_parts(&self.inner) else {
217 return self.inner == other.inner;
218 };
219
220 let Ok(right) = decode_parts(&other.inner) else {
221 return false;
222 };
223
224 left.0 == right.0 && left.1 == right.1 && left.2 == right.2
225 }
226}
227
228impl Ord for Hash {
229 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
230 let Ok(left) = decode_parts(&self.inner) else {
231 return self.inner.cmp(&other.inner);
232 };
233
234 let Ok(right) = decode_parts(&other.inner) else {
235 return self.inner.cmp(&other.inner);
236 };
237
238 left.0.cmp(&right.0)
239 }
240}
241
242impl PartialOrd for Hash {
243 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
244 Some(self.cmp(other))
245 }
246}
247
248impl From<Hash> for [u8; HASH_SIZE] {
249 fn from(hash: Hash) -> [u8; HASH_SIZE] {
250 hash.inner
251 }
252}
253
254impl From<&Hash> for String {
255 fn from(hash: &Hash) -> Self {
256 hash.to_string()
257 }
258}
259
260impl From<&Hash> for Vec<u8> {
261 fn from(hash: &Hash) -> Self {
262 hash.to_vec()
263 }
264}
265
266impl TryFrom<&[u8]> for Hash {
267 type Error = HashValidationError;
268
269 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
270 Self::validate(value)
271 }
272}
273
274impl TryFrom<&str> for Hash {
275 type Error = HashValidationError;
276
277 fn try_from(value: &str) -> Result<Self, Self::Error> {
278 value.as_bytes().try_into()
279 }
280}
281
282impl Hash {
283 #[must_use]
284 pub const fn as_bytes(&self) -> &[u8; HASH_SIZE] {
285 &self.inner
286 }
287
288 #[must_use]
289 pub fn to_vec(&self) -> Vec<u8> {
290 self.inner.to_vec()
291 }
292
293 pub fn data_max_len(&self) -> Result<usize, PsHashError> {
295 let bits = &self.inner[40..46];
296 let bits = ps_base64::decode(bits);
297 let bits = bits[2..4].try_into()?;
298 let size = PackedInt::from_16_bits(bits).to_usize();
299
300 Ok(size)
301 }
302}
303
304#[must_use]
305pub fn encode_parts(parts: HashParts) -> Hash {
306 let (xored, checksum, length) = parts;
307
308 let mut vec: Vec<u8> = Vec::with_capacity(HASH_SIZE_TOTAL_BIN);
309
310 vec.extend_from_slice(&xored);
311 vec.extend_from_slice(&length.to_16_bits());
312 vec.extend_from_slice(&checksum);
313
314 Hash {
315 inner: ps_base64::sized_encode::<HASH_SIZE>(&vec),
316 }
317}
318
319#[inline]
320pub fn hash<T: AsRef<[u8]>>(data: T) -> Result<Hash, HashError> {
321 Hash::hash(data)
322}
323
324pub fn decode_parts(hash: &[u8]) -> Result<HashParts, PsHashError> {
325 if hash.len() < HASH_SIZE {
326 return Err(PsHashError::InputTooShort);
327 }
328
329 let bytes = ps_base64::decode(hash);
330
331 Ok((
332 bytes[0..HASH_SIZE_BIN].try_into()?,
333 bytes[PARITY_OFFSET..HASH_SIZE_TOTAL_BIN].try_into()?,
334 PackedInt::from_16_bits(&bytes[HASH_SIZE_BIN..PARITY_OFFSET].try_into()?),
335 ))
336}