socket_patch_core/hash/
git_sha256.rs1use sha2::{Digest, Sha256};
2use std::io;
3use tokio::io::AsyncReadExt;
4
5pub fn compute_git_sha256_from_bytes(data: &[u8]) -> String {
9 let mut hasher = Sha256::new();
10 let header = format!("blob {}\0", data.len());
11 hasher.update(header.as_bytes());
12 hasher.update(data);
13 hex::encode(hasher.finalize())
14}
15
16pub async fn compute_git_sha256_from_reader<R: tokio::io::AsyncRead + Unpin>(
28 size: u64,
29 mut reader: R,
30) -> io::Result<String> {
31 let mut hasher = Sha256::new();
32 let header = format!("blob {}\0", size);
33 hasher.update(header.as_bytes());
34
35 let mut buf = [0u8; 8192];
36 let mut total: u64 = 0;
37 loop {
38 let n = reader.read(&mut buf).await?;
39 if n == 0 {
40 break;
41 }
42 hasher.update(&buf[..n]);
43 total += n as u64;
44 }
45
46 if total != size {
47 return Err(io::Error::new(
48 io::ErrorKind::InvalidData,
49 format!(
50 "git sha256: declared size {size} does not match {total} bytes read from stream"
51 ),
52 ));
53 }
54
55 Ok(hex::encode(hasher.finalize()))
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn test_empty_content() {
64 let hash = compute_git_sha256_from_bytes(b"");
65 assert_eq!(hash.len(), 64);
67 assert_eq!(hash, compute_git_sha256_from_bytes(b""));
69 }
70
71 #[test]
76 fn test_git_known_answer_vectors() {
77 assert_eq!(
79 compute_git_sha256_from_bytes(b""),
80 "473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813",
81 );
82 assert_eq!(
84 compute_git_sha256_from_bytes(b"Hello, World!"),
85 "e118a058f018dda253bb692320c940091b15e4f19067e12fff110606a111f5da",
86 );
87 }
88
89 #[test]
90 fn test_hello_world() {
91 let content = b"Hello, World!";
92 let hash = compute_git_sha256_from_bytes(content);
93 assert_eq!(hash.len(), 64);
94
95 use sha2::{Digest, Sha256};
97 let mut expected_hasher = Sha256::new();
98 expected_hasher.update(b"blob 13\0Hello, World!");
99 let expected = hex::encode(expected_hasher.finalize());
100 assert_eq!(hash, expected);
101 }
102
103 #[test]
104 fn test_known_vector() {
105 use sha2::{Digest, Sha256};
107 let mut hasher = Sha256::new();
108 hasher.update(b"blob 0\0");
109 let expected = hex::encode(hasher.finalize());
110 assert_eq!(compute_git_sha256_from_bytes(b""), expected);
111 }
112
113 #[tokio::test]
114 async fn test_async_reader_matches_sync() {
115 let content = b"test content for async hashing";
116 let sync_hash = compute_git_sha256_from_bytes(content);
117
118 let cursor = tokio::io::BufReader::new(&content[..]);
119 let async_hash = compute_git_sha256_from_reader(content.len() as u64, cursor)
120 .await
121 .unwrap();
122
123 assert_eq!(sync_hash, async_hash);
124 }
125
126 #[tokio::test]
130 async fn test_async_reader_multiple_chunks() {
131 let content: Vec<u8> = (0..50_000u32).map(|i| (i % 251) as u8).collect();
132 let sync_hash = compute_git_sha256_from_bytes(&content);
133
134 let cursor = tokio::io::BufReader::new(&content[..]);
135 let async_hash = compute_git_sha256_from_reader(content.len() as u64, cursor)
136 .await
137 .unwrap();
138
139 assert_eq!(sync_hash, async_hash);
140 }
141
142 #[tokio::test]
146 async fn test_async_reader_size_too_large_errors() {
147 let content = b"short";
148 let cursor = tokio::io::BufReader::new(&content[..]);
149 let result = compute_git_sha256_from_reader(content.len() as u64 + 100, cursor).await;
150
151 let err = result.expect_err("size larger than stream must error");
152 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
153 }
154
155 #[tokio::test]
159 async fn test_async_reader_size_too_small_errors() {
160 let content = b"this stream is longer than declared";
161 let cursor = tokio::io::BufReader::new(&content[..]);
162 let result = compute_git_sha256_from_reader(4, cursor).await;
163
164 let err = result.expect_err("size smaller than stream must error");
165 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
166 }
167}