ps_hash/
lib.rs

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