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