ps_hash/
lib.rs

1#![allow(clippy::missing_errors_doc)]
2mod error;
3mod implementations;
4mod methods;
5pub use error::*;
6use ps_base64::base64;
7use ps_ecc::ReedSolomon;
8use ps_pint16::PackedInt;
9use sha2::{Digest, Sha256};
10
11#[cfg(test)]
12pub mod tests;
13
14pub const DIGEST_SIZE: usize = 32;
15pub const HASH_SIZE_BIN: usize = 48;
16pub const HASH_SIZE_COMPACT: usize = 42;
17pub const HASH_SIZE: usize = 64;
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/// The minimum number of characters for a Hash to still be safely recoverable.
23pub const MIN_RECOVERABLE: usize = HASH_SIZE - (PARITY as usize * 8 / 6);
24/// The minimum number of bytes for a Hash to still be safely recoverable.
25pub const MIN_RECOVERABLE_BIN: usize = HASH_SIZE_BIN - (PARITY as usize);
26
27pub const RS: ReedSolomon = match ReedSolomon::new(PARITY) {
28    Ok(rs) => rs,
29    Err(_) => panic!("Failed to construct Reed-Solomon codec."),
30};
31
32#[inline]
33#[must_use]
34pub fn sha256(data: &[u8]) -> [u8; DIGEST_SIZE] {
35    let mut hasher = Sha256::new();
36
37    hasher.update(data);
38
39    let result = hasher.finalize();
40
41    result.into()
42}
43
44#[inline]
45#[must_use]
46pub fn blake3(data: &[u8]) -> blake3::Hash {
47    blake3::hash(data)
48}
49
50#[derive(Clone, Copy)]
51#[repr(transparent)]
52pub struct Hash {
53    inner: [u8; HASH_SIZE_BIN],
54}
55
56impl Hash {
57    /// Calculates the [`Hash`] of `data`.
58    ///
59    /// # Errors
60    ///
61    /// - [`HashError::RSGenerateParityError`] is returned if generating parity fails.
62    #[allow(clippy::self_named_constructors)]
63    pub fn hash(data: impl AsRef<[u8]>) -> Result<Self, HashError> {
64        let data = data.as_ref();
65        let mut inner = [0u8; HASH_SIZE_BIN];
66
67        let sha = sha256(data);
68        let blake = blake3(data);
69
70        // XOR digests
71        for i in 0..DIGEST_SIZE {
72            inner[i] = sha[i] ^ blake.as_bytes()[i];
73        }
74
75        // Copy length
76        inner[DIGEST_SIZE..PARITY_OFFSET]
77            .copy_from_slice(&PackedInt::from_usize(data.len()).to_16_bits());
78
79        // Generate and copy parity
80        let parity = RS.generate_parity(&inner[..PARITY_OFFSET])?;
81        inner[PARITY_OFFSET..].copy_from_slice(&parity);
82
83        Ok(Self { inner })
84    }
85
86    /// Validates and corrects a binary-encoded [`Hash`].\
87    /// The correction happens on the provided [`Vec`].
88    ///
89    /// # Errors
90    ///
91    /// - [`HashValidationError::RSDecodeError`] is returned if the hash is unrecoverable.
92    pub fn validate_bin_vec(hash: &mut Vec<u8>) -> Result<Self, HashValidationError> {
93        // The constant 0xF4 is chosen arbitrarily.
94        // Using 0x00 would produce Ok(AAA...AAA) for all short inputs.
95        hash.resize(HASH_SIZE_BIN, 0xF4);
96
97        let (data, parity) = hash.split_at_mut(PARITY_OFFSET);
98
99        ReedSolomon::correct_detached_in_place(parity, data)?;
100
101        let mut inner = [0u8; HASH_SIZE_BIN];
102
103        inner.copy_from_slice(hash);
104
105        let hash = Self { inner };
106
107        Ok(hash)
108    }
109}
110impl From<Hash> for [u8; HASH_SIZE] {
111    fn from(hash: Hash) -> [u8; HASH_SIZE] {
112        base64::sized_encode(&hash.inner)
113    }
114}
115
116impl TryFrom<&[u8]> for Hash {
117    type Error = HashValidationError;
118
119    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
120        Self::validate(value)
121    }
122}
123
124impl TryFrom<&str> for Hash {
125    type Error = HashValidationError;
126
127    fn try_from(value: &str) -> Result<Self, Self::Error> {
128        value.as_bytes().try_into()
129    }
130}
131
132#[inline]
133pub fn hash(data: impl AsRef<[u8]>) -> Result<Hash, HashError> {
134    Hash::hash(data)
135}