soar_utils/
hash.rs

1use std::path::Path;
2
3use crate::error::{HashError, HashResult};
4
5/// Calculates the checksum of a file.
6///
7/// This method reads the contents of a file and computes a checksum, which is returned as a
8/// hex-encoded string. The specific hashing algorithm depends on the implementation. The
9/// default implementation uses the `blake3` crate.
10///
11/// # Arguments
12///
13/// * `file_path` - The path to the file to calculate the checksum for.
14///
15/// # Errors
16///
17/// * [`HashError::ReadFailed`] if the file cannot be read.
18///
19/// # Example
20///
21/// ```no_run
22/// use soar_utils::error::HashResult;
23/// use soar_utils::hash::calculate_checksum;
24///
25/// fn main() -> HashResult<()> {
26///     let checksum = calculate_checksum("/path/to/file")?;
27///     println!("Checksum is {}", checksum);
28///     Ok(())
29/// }
30/// ```
31pub fn calculate_checksum<P: AsRef<Path>>(file_path: P) -> HashResult<String> {
32    let file_path = file_path.as_ref();
33    let mut hasher = blake3::Hasher::new();
34    hasher.update_mmap(file_path).map_err(|err| {
35        HashError::ReadFailed {
36            path: file_path.to_path_buf(),
37            source: err,
38        }
39    })?;
40    Ok(hasher.finalize().to_hex().to_string())
41}
42
43/// Verifies the checksum of a file against an expected value.
44///
45/// This method calculates the checksum of the given file and compares it case-insensitively
46/// against the `expected` checksum string.
47///
48/// # Arguments
49///
50/// * `file_path` - The path to the file to verify the checksum for.
51/// * `expected` - The expected checksum.
52///
53/// # Errors
54///
55/// * [`HashError::ReadFailed`] if the file cannot be read.
56///
57/// # Example
58///
59/// ```no_run
60/// use soar_utils::error::HashResult;
61/// use soar_utils::hash::verify_checksum;
62///
63/// fn main() -> HashResult<()> {
64///     let result = verify_checksum("file.dat", "1234567890abcdef")?;
65///     println!("Checksum matches: {}", result);
66///     Ok(())
67/// }
68/// ```
69pub fn verify_checksum<P: AsRef<Path>>(file_path: P, expected: &str) -> HashResult<bool> {
70    let file_path = file_path.as_ref();
71    let actual = calculate_checksum(file_path)?;
72    Ok(actual.eq_ignore_ascii_case(expected))
73}
74
75/// Calculates a hash from a string input.
76///
77/// Returns a hex-encoded blake3 hash string.
78///
79/// # Arguments
80///
81/// * `input` - The string to hash.
82///
83/// # Example
84///
85/// ```
86/// use soar_utils::hash::hash_string;
87///
88/// let hash = hash_string("hello world");
89/// assert_eq!(hash.len(), 64);
90/// ```
91pub fn hash_string(input: &str) -> String {
92    let mut hasher = blake3::Hasher::new();
93    hasher.update(input.as_bytes());
94    hasher.finalize().to_hex().to_string()
95}
96
97#[cfg(test)]
98mod tests {
99    use std::io::Write;
100
101    use tempfile::NamedTempFile;
102
103    use super::{calculate_checksum, verify_checksum};
104
105    #[test]
106    fn test_calculate_checksum() {
107        let mut file = NamedTempFile::new().unwrap();
108        file.write_all(b"hello world\n").unwrap();
109        let path = file.path();
110
111        let checksum = calculate_checksum(path).unwrap();
112        assert_eq!(
113            checksum,
114            "dc5a4edb8240b018124052c330270696f96771a63b45250a5c17d3000e823355"
115        );
116    }
117
118    #[test]
119    fn test_verify_checksum_valid() {
120        let mut file = NamedTempFile::new().unwrap();
121        file.write_all(b"hello world\n").unwrap();
122        let path = file.path();
123
124        let result = verify_checksum(
125            path,
126            "dc5a4edb8240b018124052c330270696f96771a63b45250a5c17d3000e823355",
127        )
128        .unwrap();
129        assert!(result);
130    }
131
132    #[test]
133    fn test_verify_checksum_invalid() {
134        let mut file = NamedTempFile::new().unwrap();
135        file.write_all(b"hello world").unwrap();
136        let path = file.path();
137
138        let result = verify_checksum(path, "invalid-checksum").unwrap();
139        assert!(!result);
140    }
141
142    #[test]
143    fn test_calculate_checksum_file_not_found() {
144        let result = calculate_checksum("/path/to/nonexistent/file");
145        assert!(result.is_err());
146    }
147
148    #[test]
149    fn test_verify_checksum_file_not_found() {
150        let result = verify_checksum("/path/to/nonexistent/file", "any-checksum");
151        assert!(result.is_err());
152    }
153
154    #[test]
155    fn test_calculate_checksum_on_directory() {
156        let dir = tempfile::tempdir().unwrap();
157        let result = calculate_checksum(dir.path());
158        assert!(result.is_err());
159    }
160}