use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ContentHash(pub String);
impl fmt::Display for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl ContentHash {
#[must_use]
pub fn of_bytes(content: &[u8]) -> Self {
let mut hasher = Sha256::new();
hasher.update(content);
let result = hasher.finalize();
Self(hex_encode(&result))
}
#[must_use]
pub fn of_str(content: &str) -> Self {
Self::of_bytes(content.as_bytes())
}
pub fn of_file(path: &Path) -> io::Result<Self> {
let content = fs::read(path)?;
Ok(Self::of_bytes(&content))
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HashManifest {
pub hashes: HashMap<String, ContentHash>,
}
impl HashManifest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, module_id: String, hash: ContentHash) {
self.hashes.insert(module_id, hash);
}
#[must_use]
pub fn get(&self, module_id: &str) -> Option<&ContentHash> {
self.hashes.get(module_id)
}
#[must_use]
pub fn changed_modules(&self, current: &HashManifest) -> Vec<String> {
let mut changed = Vec::new();
for (module_id, new_hash) in ¤t.hashes {
match self.hashes.get(module_id) {
Some(old_hash) if old_hash == new_hash => {}
_ => changed.push(module_id.clone()),
}
}
for module_id in self.hashes.keys() {
if !current.hashes.contains_key(module_id) {
changed.push(module_id.clone());
}
}
changed
}
#[must_use]
pub fn len(&self) -> usize {
self.hashes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.hashes.is_empty()
}
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_deterministic() {
let h1 = ContentHash::of_str("hello world");
let h2 = ContentHash::of_str("hello world");
assert_eq!(h1, h2);
}
#[test]
fn hash_differs_for_different_content() {
let h1 = ContentHash::of_str("hello");
let h2 = ContentHash::of_str("world");
assert_ne!(h1, h2);
}
#[test]
fn hash_is_hex_encoded_sha256() {
let h = ContentHash::of_str("");
assert_eq!(
h.0,
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(h.0.len(), 64);
}
#[test]
fn hash_manifest_changed_modules() {
let mut old = HashManifest::new();
old.insert("A".to_string(), ContentHash::of_str("v1"));
old.insert("B".to_string(), ContentHash::of_str("v1"));
old.insert("C".to_string(), ContentHash::of_str("v1"));
let mut new = HashManifest::new();
new.insert("A".to_string(), ContentHash::of_str("v1")); new.insert("B".to_string(), ContentHash::of_str("v2")); new.insert("D".to_string(), ContentHash::of_str("v1"));
let mut changed = old.changed_modules(&new);
changed.sort();
assert_eq!(changed, vec!["B", "C", "D"]);
}
#[test]
fn hash_manifest_empty() {
let old = HashManifest::new();
let new = HashManifest::new();
assert!(old.changed_modules(&new).is_empty());
}
#[test]
fn hash_manifest_all_new() {
let old = HashManifest::new();
let mut new = HashManifest::new();
new.insert("A".to_string(), ContentHash::of_str("v1"));
new.insert("B".to_string(), ContentHash::of_str("v1"));
let mut changed = old.changed_modules(&new);
changed.sort();
assert_eq!(changed, vec!["A", "B"]);
}
#[test]
fn hash_of_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.bock");
fs::write(&path, "fn main() {}").unwrap();
let h1 = ContentHash::of_file(&path).unwrap();
let h2 = ContentHash::of_str("fn main() {}");
assert_eq!(h1, h2);
}
}