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