use std::fmt;
use std::io::Read;
use std::path::Path;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ContentHash(pub [u8; 32]);
#[derive(Debug, thiserror::Error)]
#[error("invalid content hash")]
pub struct ParseHashError;
impl ContentHash {
pub fn of_bytes(bytes: &[u8]) -> Self {
Self(Sha256::digest(bytes).into())
}
pub fn of_file(path: &Path) -> std::io::Result<Self> {
let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(Self(hasher.finalize().into()))
}
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{b:02x}")).collect()
}
}
impl fmt::Display for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl fmt::Debug for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ContentHash({})", self.to_hex())
}
}
impl FromStr for ContentHash {
type Err = ParseHashError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != 64 {
return Err(ParseHashError);
}
let mut out = [0u8; 32];
for (i, byte) in out.iter_mut().enumerate() {
let pair = s.get(2 * i..2 * i + 2).ok_or(ParseHashError)?;
*byte = u8::from_str_radix(pair, 16).map_err(|_| ParseHashError)?;
}
Ok(Self(out))
}
}
impl Serialize for ContentHash {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> Deserialize<'de> for ContentHash {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_sha256() {
assert_eq!(
ContentHash::of_bytes(b"abc").to_hex(),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn file_hash_matches_bytes_hash() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("f");
let data = vec![7u8; 200_000];
std::fs::write(&path, &data).unwrap();
assert_eq!(
ContentHash::of_file(&path).unwrap(),
ContentHash::of_bytes(&data)
);
}
#[test]
fn hex_round_trips_through_serde() {
let h = ContentHash::of_bytes(b"x");
let json = serde_json::to_string(&h).unwrap();
assert_eq!(json, format!("\"{}\"", h.to_hex()));
assert_eq!(serde_json::from_str::<ContentHash>(&json).unwrap(), h);
assert!("zz".parse::<ContentHash>().is_err());
}
}