use sha2::{Digest, Sha256};
use std::io;
use tokio::io::AsyncReadExt;
pub fn compute_git_sha256_from_bytes(data: &[u8]) -> String {
let mut hasher = Sha256::new();
let header = format!("blob {}\0", data.len());
hasher.update(header.as_bytes());
hasher.update(data);
hex::encode(hasher.finalize())
}
pub async fn compute_git_sha256_from_reader<R: tokio::io::AsyncRead + Unpin>(
size: u64,
mut reader: R,
) -> io::Result<String> {
let mut hasher = Sha256::new();
let header = format!("blob {}\0", size);
hasher.update(header.as_bytes());
let mut buf = [0u8; 8192];
let mut total: u64 = 0;
loop {
let n = reader.read(&mut buf).await?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
total += n as u64;
}
if total != size {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"git sha256: declared size {size} does not match {total} bytes read from stream"
),
));
}
Ok(hex::encode(hasher.finalize()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_content() {
let hash = compute_git_sha256_from_bytes(b"");
assert_eq!(hash.len(), 64);
assert_eq!(hash, compute_git_sha256_from_bytes(b""));
}
#[test]
fn test_git_known_answer_vectors() {
assert_eq!(
compute_git_sha256_from_bytes(b""),
"473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813",
);
assert_eq!(
compute_git_sha256_from_bytes(b"Hello, World!"),
"e118a058f018dda253bb692320c940091b15e4f19067e12fff110606a111f5da",
);
}
#[test]
fn test_hello_world() {
let content = b"Hello, World!";
let hash = compute_git_sha256_from_bytes(content);
assert_eq!(hash.len(), 64);
use sha2::{Digest, Sha256};
let mut expected_hasher = Sha256::new();
expected_hasher.update(b"blob 13\0Hello, World!");
let expected = hex::encode(expected_hasher.finalize());
assert_eq!(hash, expected);
}
#[test]
fn test_known_vector() {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(b"blob 0\0");
let expected = hex::encode(hasher.finalize());
assert_eq!(compute_git_sha256_from_bytes(b""), expected);
}
#[tokio::test]
async fn test_async_reader_matches_sync() {
let content = b"test content for async hashing";
let sync_hash = compute_git_sha256_from_bytes(content);
let cursor = tokio::io::BufReader::new(&content[..]);
let async_hash = compute_git_sha256_from_reader(content.len() as u64, cursor)
.await
.unwrap();
assert_eq!(sync_hash, async_hash);
}
#[tokio::test]
async fn test_async_reader_multiple_chunks() {
let content: Vec<u8> = (0..50_000u32).map(|i| (i % 251) as u8).collect();
let sync_hash = compute_git_sha256_from_bytes(&content);
let cursor = tokio::io::BufReader::new(&content[..]);
let async_hash = compute_git_sha256_from_reader(content.len() as u64, cursor)
.await
.unwrap();
assert_eq!(sync_hash, async_hash);
}
#[tokio::test]
async fn test_async_reader_size_too_large_errors() {
let content = b"short";
let cursor = tokio::io::BufReader::new(&content[..]);
let result = compute_git_sha256_from_reader(content.len() as u64 + 100, cursor).await;
let err = result.expect_err("size larger than stream must error");
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[tokio::test]
async fn test_async_reader_size_too_small_errors() {
let content = b"this stream is longer than declared";
let cursor = tokio::io::BufReader::new(&content[..]);
let result = compute_git_sha256_from_reader(4, cursor).await;
let err = result.expect_err("size smaller than stream must error");
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
}