//! Two functions, shared by two real callers (`verifier`, `builder`) — //! not a general-purpose utils dump. See docs/ARCHITECTURE.md > "organize by //! pipeline stage, not by layer" for why that distinction matters. use anyhow::{Context, Result}; use sha2::{Digest, Sha256}; use std::io::Read; use std::path::Path; /// In-memory digest — test-only now that both real callers (`verifier`, /// `builder`) hash a file already on disk via `sha256_hex_file` instead. /// Kept for building expected hashes from in-memory test fixtures. #[cfg(test)] pub(crate) fn sha256_hex(data: &[u8]) -> String { let mut hasher = Sha256::new(); hasher.update(data); hex::encode(hasher.finalize()) } /// Same digest as `sha256_hex(&std::fs::read(path)?)`, but streamed in /// fixed-size chunks instead of reading the whole file into memory first — /// downloaded release assets are tens of MB, and the file is already on /// disk, so there's no reason to hold a second full copy in memory just to /// hash it. pub fn sha256_hex_file(path: &Path) -> Result { let mut file = std::fs::File::open(path).with_context(|| format!("opening {}", path.display()))?; let mut hasher = Sha256::new(); let mut buf = [0u8; 64 * 1024]; loop { let n = file .read(&mut buf) .with_context(|| format!("reading {}", path.display()))?; if n == 0 { break; } hasher.update(&buf[..n]); } Ok(hex::encode(hasher.finalize())) } #[cfg(test)] mod tests { use super::*; #[test] fn matches_known_sha256() { // printf 'hello world' | sha256sum assert_eq!( sha256_hex(b"hello world"), "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" ); } #[test] fn sha256_hex_file_matches_in_memory_digest() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("data.bin"); // Bigger than one read chunk, to actually exercise the loop. let data = vec![0x5au8; 200 * 1024]; std::fs::write(&path, &data).unwrap(); assert_eq!(sha256_hex_file(&path).unwrap(), sha256_hex(&data)); } #[test] fn sha256_hex_file_matches_for_empty_file() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("empty.bin"); std::fs::write(&path, b"").unwrap(); assert_eq!(sha256_hex_file(&path).unwrap(), sha256_hex(b"")); } #[test] fn sha256_hex_file_errors_on_missing_file() { let dir = tempfile::tempdir().unwrap(); let missing = dir.path().join("does-not-exist.bin"); assert!(sha256_hex_file(&missing).is_err()); } }