ps_hash/
lib.rs

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/// a 64-byte ascii string representing a Hash
49#[derive(Clone, Copy, Eq)]
50#[repr(transparent)]
51pub struct Hash {
52    inner: [u8; HASH_SIZE],
53}
54
55impl Hash {
56    /// Calculated the [`Hash`] of `data`.
57    ///
58    /// # Errors
59    ///
60    /// - [`HashError::BufferError`] is returned if an allocation fails.
61    /// - [`HashError::RSGenerateParityError`] is returned if generating parity fails.
62    #[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    /// Validates and corrects a [`Hash`].
80    ///
81    /// # Errors
82    ///
83    /// - [`HashValidationError::RSDecodeError`] is returned if the hash is unrecoverable.
84    pub fn validate<T: AsRef<[u8]>>(hash: T) -> Result<Self, HashValidationError> {
85        let mut hash = base64::decode(hash.as_ref());
86
87        // The constant 0xF4 is chosen arbitrarily.
88        // Using 0x00 would produce Ok(AAA...AAA) for all short inputs.
89        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    /// This should tell you how large a vector to allocate if you want to copy the hashed data.
294    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}