esvc_core/
hash.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// When dealing with new events which use a different hash than others,
5/// keep in mind that the hash will thus differ, and they won't be merged
6/// inside of the graph. This can be mitigated by migrating all graph nodes
7/// or by strictly reusing graph nodes, however, the performance penality
8/// might massively exceed the compatiblity benefit.
9#[repr(C)]
10#[serde_with::serde_as]
11#[derive(Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum Hash {
13    Blake2b512(#[serde_as(as = "serde_with::Bytes")] [u8; 64]),
14}
15
16const HASH_B64_CFG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
17const HASH_BLK2512_PFX: &str = "blake2b512:";
18
19impl fmt::Display for Hash {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        let (kind, bytes) = match self {
22            Hash::Blake2b512(ref x) => (HASH_BLK2512_PFX, x),
23        };
24        write!(f, "{}{}", kind, base64::encode_config(bytes, HASH_B64_CFG))
25    }
26}
27
28impl fmt::Debug for Hash {
29    // forward to Display impl because it's more readable
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        <Hash as fmt::Display>::fmt(self, f)
32    }
33}
34
35#[derive(Clone, Debug, thiserror::Error, PartialEq)]
36pub enum HashDecodeError {
37    #[error("base64 decoding error: {0}")]
38    Base64(#[from] base64::DecodeError),
39
40    #[error("concrete hash part is too short (got {got}, expected {expected})")]
41    TooShort { got: usize, expected: usize },
42
43    #[error("invalid hash prefix '{0}'")]
44    InvalidPrefix(String),
45}
46
47impl core::str::FromStr for Hash {
48    type Err = HashDecodeError;
49
50    fn from_str(s: &str) -> Result<Hash, HashDecodeError> {
51        if let Some(x) = s.strip_prefix(HASH_BLK2512_PFX) {
52            let mut buf = [0u8; 64];
53            let dcl = base64::decode_config_slice(x, HASH_B64_CFG, &mut buf).map_err(|x| {
54                use base64::DecodeError as Bdce;
55                let offset = HASH_BLK2512_PFX.len();
56                match x {
57                    Bdce::InvalidByte(a, b) => Bdce::InvalidByte(offset + a, b),
58                    Bdce::InvalidLength => Bdce::InvalidLength,
59                    Bdce::InvalidLastSymbol(a, b) => Bdce::InvalidLastSymbol(offset + a, b),
60                }
61            })?;
62            if dcl < buf.len() {
63                return Err(HashDecodeError::TooShort {
64                    got: x.len(),
65                    expected: buf.len(),
66                });
67            }
68            Ok(Hash::Blake2b512(buf))
69        } else {
70            let truncp = s.find(':').unwrap_or(s.len());
71            Err(HashDecodeError::InvalidPrefix(s[..truncp].to_string()))
72        }
73    }
74}
75
76// TODO: make it possible to select which hash should be used
77pub fn calculate_hash(dat: &[u8]) -> Hash {
78    use blake2::Digest;
79    let mut hasher = blake2::Blake2b512::new();
80    hasher.update(dat);
81    let tmp = hasher.finalize();
82    let mut ret = [0u8; 64];
83    ret.copy_from_slice(tmp.as_slice());
84    Hash::Blake2b512(ret)
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn hash_parse_err_invalid_prefix() {
93        assert_eq!(
94            "hello:1234".parse::<Hash>(),
95            Err(HashDecodeError::InvalidPrefix("hello".to_string()))
96        );
97    }
98
99    #[test]
100    fn hash_parse_err_base64() {
101        assert_eq!(
102            "blake2b512:.".parse::<Hash>(),
103            Err(HashDecodeError::Base64(base64::DecodeError::InvalidByte(
104                11, b'.'
105            )))
106        );
107    }
108
109    const GTH: Hash = Hash::Blake2b512([
110        207, 114, 247, 238, 107, 232, 17, 55, 229, 186, 214, 166, 184, 208, 96, 252, 67, 32, 28,
111        203, 113, 194, 111, 24, 149, 157, 137, 127, 183, 118, 121, 156, 14, 32, 34, 132, 138, 243,
112        141, 153, 87, 76, 109, 145, 247, 109, 108, 230, 13, 210, 5, 38, 56, 76, 18, 41, 96, 233,
113        122, 235, 55, 66, 107, 150,
114    ]);
115
116    #[test]
117    fn ex0_calc_hash() {
118        assert_eq!(calculate_hash("Guten Tag!".as_bytes()), GTH);
119    }
120
121    const GTH_STR: &str = "blake2b512:z3L37mvoETflutamuNBg_EMgHMtxwm8YlZ2Jf7d2eZwOICKEivONmVdMbZH3bWzmDdIFJjhMEilg6XrrN0Jrlg";
122
123    #[test]
124    fn ex0_hash_str() {
125        assert_eq!(GTH.to_string(), GTH_STR);
126        assert_eq!(GTH_STR.parse::<Hash>(), Ok(GTH));
127    }
128}