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)]
20pub 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)]
49pub struct HashConfig {
51 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}