Skip to main content

pedant_core/
hash.rs

1use std::collections::BTreeMap;
2use std::fmt::Write;
3
4use sha2::{Digest, Sha256};
5
6/// SHA-256 digest of source paths and contents, iterated in key order.
7///
8/// The `BTreeMap` guarantees deterministic iteration regardless of insertion order.
9pub fn compute_source_hash<K: Ord + AsRef<str>, V: AsRef<str>>(
10    sources: &BTreeMap<K, V>,
11) -> Box<str> {
12    let mut hasher = Sha256::new();
13    for (path, content) in sources {
14        update_hash_entry(&mut hasher, path.as_ref(), content.as_ref());
15    }
16    let digest = hasher.finalize();
17    encode_hex_digest(&digest)
18}
19
20/// The raw SHA-256 digest of one byte run.
21///
22/// Kept beside [`compute_source_hash`] because both answer the same question
23/// about bytes; the difference is only whether the caller wants the digest or
24/// its rendering. Manifest freshness and semantic-snapshot verification both
25/// compare digests rather than hex, so they read this one.
26pub fn digest_bytes(bytes: &[u8]) -> [u8; 32] {
27    let mut hasher = Sha256::new();
28    hasher.update(bytes);
29    hasher.finalize().into()
30}
31
32fn update_hash_entry(hasher: &mut Sha256, path: &str, content: &str) {
33    let path_bytes = path.as_bytes();
34    let content_bytes = content.as_bytes();
35
36    hasher.update((path_bytes.len() as u64).to_be_bytes());
37    hasher.update(path_bytes);
38    hasher.update((content_bytes.len() as u64).to_be_bytes());
39    hasher.update(content_bytes);
40}
41
42/// Lowercase hex encoding of a digest's bytes.
43pub fn encode_hex_digest(digest: &[u8]) -> Box<str> {
44    let mut hex: String = String::with_capacity(64);
45    for byte in digest {
46        // write! to String is infallible; the Result is always Ok
47        write!(hex, "{byte:02x}").ok();
48    }
49    Box::from(hex)
50}