havocompare/
hash.rs

1use std::fs::File;
2use std::io::Read;
3use std::path::{Path, PathBuf};
4
5use data_encoding::HEXLOWER;
6use schemars_derive::JsonSchema;
7use thiserror::Error;
8use vg_errortools::fat_io_wrap_std;
9use vg_errortools::FatIOError;
10
11use crate::report::{DiffDetail, Difference};
12use crate::{report, Deserialize, Serialize};
13
14#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Copy)]
15pub enum HashFunction {
16    Sha256,
17}
18
19#[derive(Debug, Error)]
20/// Errors during hash checking
21pub enum Error {
22    #[error("Failed to compile regex {0}")]
23    RegexCompilationFailed(#[from] regex::Error),
24    #[error("Problem creating hash report {0}")]
25    ReportingFailure(#[from] report::Error),
26    #[error("File access failed {0}")]
27    FileAccessProblem(#[from] FatIOError),
28}
29
30impl HashFunction {
31    fn hash_file(&self, mut file: impl Read) -> Result<[u8; 32], Error> {
32        match self {
33            Self::Sha256 => {
34                use sha2::{Digest, Sha256};
35                use std::io;
36
37                let mut hasher = Sha256::new();
38
39                let _ = io::copy(&mut file, &mut hasher)
40                    .map_err(|e| FatIOError::from_std_io_err(e, PathBuf::new()))?;
41                let hash_bytes = hasher.finalize();
42                Ok(hash_bytes.into())
43            }
44        }
45    }
46}
47
48#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
49/// Configuration options for the hash comparison module
50pub struct HashConfig {
51    /// Which hash function to use
52    pub function: HashFunction,
53}
54
55impl Default for HashConfig {
56    fn default() -> Self {
57        HashConfig {
58            function: HashFunction::Sha256,
59        }
60    }
61}
62
63pub fn compare_files<P: AsRef<Path>>(
64    nominal_path: P,
65    actual_path: P,
66    config: &HashConfig,
67) -> Result<Difference, Error> {
68    let act = config
69        .function
70        .hash_file(fat_io_wrap_std(actual_path.as_ref(), &File::open)?)?;
71    let nom = config
72        .function
73        .hash_file(fat_io_wrap_std(nominal_path.as_ref(), &File::open)?)?;
74
75    let mut difference = Difference::new_for_file(nominal_path, actual_path);
76    if act != nom {
77        difference.push_detail(DiffDetail::Hash {
78            actual: HEXLOWER.encode(&act),
79            nominal: HEXLOWER.encode(&nom),
80        });
81        difference.error();
82    }
83    Ok(difference)
84}
85
86#[cfg(test)]
87mod test {
88    use crate::hash::HashFunction::Sha256;
89
90    use super::*;
91
92    #[test]
93    fn identity() {
94        let f1 = Sha256
95            .hash_file(File::open("tests/integ.rs").unwrap())
96            .unwrap();
97        let f2 = Sha256
98            .hash_file(File::open("tests/integ.rs").unwrap())
99            .unwrap();
100        assert_eq!(f1, f2);
101    }
102
103    #[test]
104    fn hash_pinning() {
105        let sum = "378f768a589f29fcbd23835ec4764a53610fc910e60b540e1e5204bdaf2c73a0";
106        let f1 = Sha256
107            .hash_file(File::open("tests/integ/data/images/diff_100_DPI.png").unwrap())
108            .unwrap();
109        assert_eq!(HEXLOWER.encode(&f1), sum);
110    }
111
112    #[test]
113    fn identity_outer() {
114        let file = "tests/integ.rs";
115        let result = compare_files(file, file, &HashConfig::default()).unwrap();
116        assert!(!result.is_error);
117    }
118
119    #[test]
120    fn different_files_throw_outer() {
121        let file_act = "tests/integ/data/images/actual/SaveImage_100DPI_default_size.jpg";
122        let file_nominal = "tests/integ/data/images/expected/SaveImage_100DPI_default_size.jpg";
123
124        let result = compare_files(file_act, file_nominal, &HashConfig::default()).unwrap();
125        assert!(result.is_error);
126    }
127}