Skip to main content

sapphire_framework_sync/
hash.rs

1//! Content addressing.
2
3use std::fmt;
4use std::io::Read;
5use std::path::Path;
6use std::str::FromStr;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use sha2::{Digest, Sha256};
10
11/// SHA-256 of a file's bytes.
12#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct ContentHash(pub [u8; 32]);
14
15/// A string that is not 64 hex digits.
16#[derive(Debug, thiserror::Error)]
17#[error("invalid content hash")]
18pub struct ParseHashError;
19
20impl ContentHash {
21    pub fn of_bytes(bytes: &[u8]) -> Self {
22        Self(Sha256::digest(bytes).into())
23    }
24
25    pub fn of_file(path: &Path) -> std::io::Result<Self> {
26        let mut file = std::fs::File::open(path)?;
27        let mut hasher = Sha256::new();
28        let mut buf = vec![0u8; 64 * 1024];
29        loop {
30            let n = file.read(&mut buf)?;
31            if n == 0 {
32                break;
33            }
34            hasher.update(&buf[..n]);
35        }
36        Ok(Self(hasher.finalize().into()))
37    }
38
39    pub fn to_hex(&self) -> String {
40        self.0.iter().map(|b| format!("{b:02x}")).collect()
41    }
42}
43
44impl fmt::Display for ContentHash {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str(&self.to_hex())
47    }
48}
49
50impl fmt::Debug for ContentHash {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(f, "ContentHash({})", self.to_hex())
53    }
54}
55
56impl FromStr for ContentHash {
57    type Err = ParseHashError;
58
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        if s.len() != 64 {
61            return Err(ParseHashError);
62        }
63        let mut out = [0u8; 32];
64        for (i, byte) in out.iter_mut().enumerate() {
65            let pair = s.get(2 * i..2 * i + 2).ok_or(ParseHashError)?;
66            *byte = u8::from_str_radix(pair, 16).map_err(|_| ParseHashError)?;
67        }
68        Ok(Self(out))
69    }
70}
71
72impl Serialize for ContentHash {
73    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
74        serializer.serialize_str(&self.to_hex())
75    }
76}
77
78impl<'de> Deserialize<'de> for ContentHash {
79    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
80        let s = String::deserialize(deserializer)?;
81        s.parse().map_err(serde::de::Error::custom)
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn known_sha256() {
91        assert_eq!(
92            ContentHash::of_bytes(b"abc").to_hex(),
93            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
94        );
95    }
96
97    #[test]
98    fn file_hash_matches_bytes_hash() {
99        let dir = tempfile::tempdir().unwrap();
100        let path = dir.path().join("f");
101        let data = vec![7u8; 200_000];
102        std::fs::write(&path, &data).unwrap();
103        assert_eq!(
104            ContentHash::of_file(&path).unwrap(),
105            ContentHash::of_bytes(&data)
106        );
107    }
108
109    #[test]
110    fn hex_round_trips_through_serde() {
111        let h = ContentHash::of_bytes(b"x");
112        let json = serde_json::to_string(&h).unwrap();
113        assert_eq!(json, format!("\"{}\"", h.to_hex()));
114        assert_eq!(serde_json::from_str::<ContentHash>(&json).unwrap(), h);
115        assert!("zz".parse::<ContentHash>().is_err());
116    }
117}