Skip to main content

sigmf/utils/
hash.rs

1//! SHA-512 hashing helpers.
2//!
3//! SigMF records the SHA-512 digest of a dataset in `core:sha512`. These
4//! helpers compute digests and return them as 128-character lowercase
5//! hexadecimal strings.
6
7use crate::SigmfError;
8use sha2::Digest;
9use std::io::Read;
10
11/// Computes the SHA-512 digest of everything remaining in `reader`.
12///
13/// The reader is consumed in 64 KiB chunks until end of input; reads that
14/// fail with [`std::io::ErrorKind::Interrupted`] are retried.
15///
16/// # Errors
17///
18/// Returns [`SigmfError::Io`] if reading fails.
19pub fn hash_from_reader(reader: &mut dyn Read) -> Result<String, SigmfError> {
20    let mut chunk = [0u8; 64 * 1024];
21
22    let mut hasher = sha2::Sha512::new();
23    loop {
24        match reader.read(&mut chunk) {
25            Ok(0) => break, // end of file
26            Ok(n) => hasher.update(&chunk[..n]),
27            Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
28            Err(err) => return Err(SigmfError::Io(err)),
29        }
30    }
31    let hash = hasher.finalize();
32    let hash_str = hash.iter().map(|byte| format!("{:02x}", byte)).collect();
33    Ok(hash_str)
34}
35
36/// Computes the SHA-512 digest of the file at `path`.
37///
38/// # Errors
39///
40/// Returns [`SigmfError::Io`] if the file cannot be opened or read.
41pub fn hash_from_path(path: impl AsRef<std::path::Path>) -> Result<String, SigmfError> {
42    let file = std::fs::File::open(path)?;
43    let mut reader = std::io::BufReader::new(file);
44    let hash = hash_from_reader(&mut reader)?;
45    Ok(hash)
46}
47
48/// Computes the SHA-512 digest of `data`.
49///
50/// This currently never fails; the [`Result`] is kept for consistency with
51/// the other hashing helpers.
52///
53/// # Examples
54///
55/// ```rust
56/// use sigmf::utils::hash::{hash_from_reader, hash_from_slice};
57///
58/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
59/// let digest = hash_from_slice(b"abc")?;
60/// assert_eq!(digest.len(), 128);
61/// assert!(digest.starts_with("ddaf35a193617aba"));
62///
63/// let mut reader = std::io::Cursor::new(b"abc".to_vec());
64/// assert_eq!(hash_from_reader(&mut reader)?, digest);
65/// # Ok(())
66/// # }
67/// ```
68pub fn hash_from_slice(data: &[u8]) -> Result<String, SigmfError> {
69    let mut hasher = sha2::Sha512::new();
70    hasher.update(data);
71    let hash = hasher.finalize();
72    let hash_str = hash.iter().map(|byte| format!("{:02x}", byte)).collect();
73    Ok(hash_str)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use std::io::{self, Cursor};
80
81    struct InterruptedShortReader {
82        cursor: Cursor<Vec<u8>>,
83        interrupted: bool,
84    }
85
86    impl Read for InterruptedShortReader {
87        fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
88            if !self.interrupted {
89                self.interrupted = true;
90                return Err(io::ErrorKind::Interrupted.into());
91            }
92            let limit = buffer.len().min(3);
93            self.cursor.read(&mut buffer[..limit])
94        }
95    }
96
97    #[test]
98    fn hashes_a_self_contained_temporary_fixture() {
99        let path = std::env::temp_dir().join(format!(
100            "sigmf-hash-test-{}-{}",
101            std::process::id(),
102            std::time::SystemTime::now()
103                .duration_since(std::time::UNIX_EPOCH)
104                .unwrap()
105                .as_nanos()
106        ));
107        let fixture = b"sigmf hash fixture";
108        std::fs::write(&path, fixture).unwrap();
109        let hash = hash_from_path(&path).unwrap();
110        std::fs::remove_file(path).unwrap();
111        assert_eq!(hash, hash_from_slice(fixture).unwrap());
112    }
113
114    #[test]
115    fn reader_hash_retries_interrupted_and_short_reads() {
116        let fixture = b"short and interrupted";
117        let mut reader = InterruptedShortReader {
118            cursor: Cursor::new(fixture.to_vec()),
119            interrupted: false,
120        };
121        assert_eq!(
122            hash_from_reader(&mut reader).unwrap(),
123            hash_from_slice(fixture).unwrap()
124        );
125    }
126}