use alloc::string::String;
use core::fmt::Write as _;
use sha2::{Digest, Sha256};
pub const SHA256_LEN: usize = 32;
pub fn sha256(bytes: &[u8]) -> [u8; SHA256_LEN] {
Sha256::digest(bytes).into()
}
pub fn sha256_hex(bytes: &[u8]) -> String {
format_digest(sha256(bytes))
}
pub fn format_digest(bytes: impl IntoIterator<Item = u8>) -> String {
let mut output = String::with_capacity(SHA256_LEN * 2);
for byte in bytes {
write!(&mut output, "{byte:02x}").expect("writing to a String cannot fail");
}
output
}
#[cfg(feature = "std")]
pub fn sha256_path(path: &std::path::Path) -> std::io::Result<String> {
use std::io::Read as _;
let mut file = std::fs::File::open(path)?;
let mut digest = Sha256::new();
let mut buffer = [0_u8; 8192];
loop {
let count = file.read(&mut buffer)?;
if count == 0 {
break;
}
digest.update(&buffer[..count]);
}
Ok(format_digest(digest.finalize()))
}
#[cfg(test)]
mod tests {
use super::{format_digest, sha256, sha256_hex};
use alloc::string::String;
const EMPTY_SHA256: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
const ABC_SHA256: &str = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
#[test]
fn known_answer_vectors_hold() {
assert_eq!(sha256_hex(b""), EMPTY_SHA256);
assert_eq!(sha256_hex(b"abc"), ABC_SHA256);
}
#[test]
fn hex_form_is_lowercase_and_zero_padded() {
assert_eq!(format_digest([0x00, 0x0f, 0xa0, 0xff]), "000fa0ff");
assert_eq!(sha256_hex(b"abc").len(), 64);
assert!(
sha256_hex(b"abc")
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
);
}
#[test]
fn sha256_and_sha256_hex_are_the_same_digest() {
assert_eq!(format_digest(sha256(b"abc")), sha256_hex(b"abc"));
assert_eq!(sha256(b"abc").len(), super::SHA256_LEN);
}
#[test]
fn formatting_matches_every_encoder_it_replaced() {
fn fold_with_write(bytes: &[u8]) -> String {
use core::fmt::Write;
bytes.iter().fold(String::new(), |mut output, byte| {
write!(&mut output, "{byte:02x}").expect("writing a String cannot fail");
output
})
}
fn map_and_collect(bytes: &[u8]) -> String {
use alloc::format;
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
fn nibble_table(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
output.push(HEX[usize::from(byte >> 4)] as char);
output.push(HEX[usize::from(byte & 0x0f)] as char);
}
output
}
for input in [b"".as_slice(), b"abc", b"\x00\xff\x0f\xa0", b"shepherd"] {
let digest = sha256(input);
let expected = format_digest(digest);
assert_eq!(expected, fold_with_write(&digest));
assert_eq!(expected, map_and_collect(&digest));
assert_eq!(expected, nibble_table(&digest));
assert_eq!(expected, sha256_hex(input));
}
}
#[cfg(feature = "std")]
#[test]
fn sha256_path_streams_the_same_digest_as_sha256_hex() {
use super::sha256_path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_nanos())
.unwrap_or(0);
let long = b"shepherd"
.iter()
.copied()
.cycle()
.take(20_000)
.collect::<Vec<u8>>();
for contents in [Vec::new(), b"abc".to_vec(), long] {
let path = std::env::temp_dir().join(format!(
"shepherd-core-digest-{stamp}-{}.bin",
COUNTER.fetch_add(1, Ordering::Relaxed)
));
std::fs::write(&path, &contents).expect("write scratch fixture");
let streamed = sha256_path(&path).expect("hash scratch fixture");
let _ = std::fs::remove_file(&path);
assert_eq!(streamed, sha256_hex(&contents));
}
}
#[cfg(feature = "std")]
#[test]
fn sha256_path_surfaces_the_io_error_it_was_handed() {
use super::sha256_path;
let missing = std::env::temp_dir().join("shepherd-core-digest-does-not-exist.bin");
let error = sha256_path(&missing).expect_err("a missing file cannot be hashed");
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
}
}