sapphire_framework_sync/
hash.rs1use 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#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct ContentHash(pub [u8; 32]);
14
15#[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}