1use crate::error::Result;
4use std::fs::File;
5use std::io::{BufReader, Read};
6use std::path::Path;
7
8pub const HASH_SIZE: usize = 32;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct DocumentHash([u8; HASH_SIZE]);
14
15impl DocumentHash {
16 pub fn from_bytes(bytes: [u8; HASH_SIZE]) -> Self {
18 Self(bytes)
19 }
20
21 pub fn as_bytes(&self) -> &[u8; HASH_SIZE] {
23 &self.0
24 }
25
26 pub fn to_base64(&self) -> String {
28 use base64::Engine;
29 base64::engine::general_purpose::STANDARD.encode(self.0)
30 }
31
32 pub fn from_base64(s: &str) -> Result<Self> {
34 use base64::Engine;
35 let bytes = base64::engine::general_purpose::STANDARD.decode(s)?;
36 if bytes.len() != HASH_SIZE {
37 return Err(crate::error::SignError::InvalidFormat(format!(
38 "Invalid hash length: expected {}, got {}",
39 HASH_SIZE,
40 bytes.len()
41 )));
42 }
43 let mut arr = [0u8; HASH_SIZE];
44 arr.copy_from_slice(&bytes);
45 Ok(Self(arr))
46 }
47
48 pub fn to_hex(&self) -> String {
50 self.0.iter().map(|b| format!("{:02x}", b)).collect()
51 }
52}
53
54pub fn hash_bytes(data: &[u8]) -> DocumentHash {
56 let hash = blake3::hash(data);
57 DocumentHash(*hash.as_bytes())
58}
59
60pub fn hash_file<P: AsRef<Path>>(path: P) -> Result<DocumentHash> {
62 let file = File::open(path)?;
63 let mut reader = BufReader::new(file);
64 hash_reader(&mut reader)
65}
66
67pub fn hash_reader<R: Read>(reader: &mut R) -> Result<DocumentHash> {
69 let mut hasher = blake3::Hasher::new();
70 let mut buffer = [0u8; 8192];
71
72 loop {
73 let bytes_read = reader.read(&mut buffer)?;
74 if bytes_read == 0 {
75 break;
76 }
77 hasher.update(&buffer[..bytes_read]);
78 }
79
80 let hash = hasher.finalize();
81 Ok(DocumentHash(*hash.as_bytes()))
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_hash_bytes() {
90 let data = b"Hello, World!";
91 let hash = hash_bytes(data);
92
93 let hash2 = hash_bytes(data);
95 assert_eq!(hash, hash2);
96
97 let hash3 = hash_bytes(b"Different data");
99 assert_ne!(hash, hash3);
100 }
101
102 #[test]
103 fn test_base64_roundtrip() {
104 let data = b"Test data for hashing";
105 let hash = hash_bytes(data);
106
107 let encoded = hash.to_base64();
108 let decoded = DocumentHash::from_base64(&encoded).unwrap();
109
110 assert_eq!(hash, decoded);
111 }
112
113 #[test]
114 fn test_hex_encoding() {
115 let data = b"Test";
116 let hash = hash_bytes(data);
117 let hex = hash.to_hex();
118
119 assert_eq!(hex.len(), 64);
121 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
122 }
123}
124