pmacs/src/hash.rs

59 lines
1.8 KiB
Rust

// hash.rs --- shared content hashing (Arc 3 phase 3, Q#AS9).
//! One `sha256_hex` for the whole crate. Three call sites want a stable,
//! filename-safe digest of a string:
//!
//! * [`crate::desktop`] — the desktop session key,
//! * [`crate::autosave`] — the recovery-file key (a hash of the path),
//! * `crate::packages::fetcher` — the package cache/mirror key.
//!
//! Each had grown (or was about to grow) its own private copy. A
//! *cryptographic* digest matters for the fetcher: a non-cryptographic
//! hash is trivially collidable, and a deliberate collision would make
//! two URLs share one bare mirror + lock file.
//!
//! Lowercase hex, so the output passes
//! [`crate::state::validate_name`]'s `[A-Za-z0-9._-]` charset.
use sha2::{Digest, Sha256};
/// Lowercase-hex SHA-256 of `s`.
pub(crate) fn sha256_hex(s: &str) -> String {
use std::fmt::Write as _;
let mut hasher = Sha256::new();
hasher.update(s.as_bytes());
let digest = hasher.finalize();
let mut out = String::with_capacity(digest.len() * 2);
for b in digest {
let _ = write!(out, "{b:02x}");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_vector_and_charset() {
// The canonical empty-string SHA-256.
assert_eq!(
sha256_hex(""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
let h = sha256_hex("/home/u/a.rs");
assert_eq!(h.len(), 64);
assert!(
h.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
);
// Filename-safe: passes the state-key charset.
assert!(crate::state::validate_name(&format!("autosave/{h}")).is_ok());
}
#[test]
fn distinct_inputs_distinct_digests() {
assert_ne!(sha256_hex("a"), sha256_hex("b"));
}
}