]> code.octet-stream.net Git - hashgood/blob - src/main.rs
Exit with non-zero exit code if a verification fails
[hashgood] / src / main.rs
1 use std::error::Error;
2 use std::path::{Path, PathBuf};
3 use std::process;
4 use structopt::StructOpt;
5
6 /// Calculate digests for given input data
7 mod calculate;
8
9 /// Display output nicely in the terminal
10 mod display;
11
12 /// Collect candidate hashes based on options and match them against a calculated hash
13 mod verify;
14
15 /// Problem running the program
16 const EXIT_ERR: i32 = 1;
17 /// Verification was performed and was not a match
18 const EXIT_MISMATCH: i32 = 2;
19
20 #[derive(StructOpt)]
21 #[structopt(name = "hashgood")]
22 pub struct Opt {
23 /// Read the hash from the clipboard
24 #[cfg(feature = "paste")]
25 #[structopt(short = "p", long = "paste")]
26 paste: bool,
27
28 /// Disable ANSI colours in output
29 #[structopt(short = "C", long = "no-colour")]
30 no_colour: bool,
31
32 /// A file containing the hash to verify. It can either be a raw hash or a SHASUMS-style listing. Use `-` for standard input.
33 #[structopt(short = "c", long = "check", parse(from_os_str))]
34 hash_file: Option<PathBuf>,
35
36 /// The file to be verified or `-` for standard input
37 #[structopt(name = "input", parse(from_os_str))]
38 input: PathBuf,
39
40 /// A hash to verify, supplied directly on the command line
41 #[structopt(name = "hash")]
42 hash: Option<String>,
43 }
44
45 impl Opt {
46 fn get_paste(&self) -> bool {
47 #[cfg(feature = "paste")]
48 {
49 self.paste
50 }
51 #[cfg(not(feature = "paste"))]
52 {
53 false
54 }
55 }
56 }
57
58 /// Types of supported digest algorithm
59 #[derive(Debug, PartialEq, Copy, Clone)]
60 pub enum Algorithm {
61 Md5,
62 Sha1,
63 Sha256,
64 }
65
66 impl Algorithm {
67 /// Assume a hash type from the binary length. Fortunately the typical 3 algorithms we care about are different lengths.
68 pub fn from_len(len: usize) -> Result<Algorithm, String> {
69 match len {
70 16 => Ok(Algorithm::Md5),
71 20 => Ok(Algorithm::Sha1),
72 32 => Ok(Algorithm::Sha256),
73 _ => Err(format!("Unrecognised hash length: {} bytes", len)),
74 }
75 }
76 }
77
78 /// The method by which one or more hashes were supplied to verify the calculated digest
79 #[derive(Debug, PartialEq)]
80 pub enum VerificationSource {
81 CommandArgument,
82 Clipboard,
83 RawFile(String),
84 DigestsFile(String),
85 }
86
87 /// A complete standalone hash result
88 pub struct Hash {
89 alg: Algorithm,
90 bytes: Vec<u8>,
91 filename: String,
92 }
93
94 impl Hash {
95 pub fn new(alg: Algorithm, bytes: Vec<u8>, path: &Path) -> Self {
96 // Taking the filename component should always work?
97 // If not, just fall back to the full path
98 let filename = match path.file_name() {
99 Some(filename) => filename.to_string_lossy(),
100 None => path.to_string_lossy(),
101 };
102 Self {
103 alg,
104 bytes,
105 filename: filename.to_string(),
106 }
107 }
108 }
109
110 /// A possible hash to match against. The algorithm is assumed.
111 #[derive(Debug, PartialEq)]
112 pub struct CandidateHash {
113 bytes: Vec<u8>,
114 filename: Option<String>,
115 }
116
117 /// A list of candidate hashes that our input could potentially match. At this point it is
118 /// assumed that we will be verifying a digest of a particular, single algorithm.
119 #[derive(Debug, PartialEq)]
120 pub struct CandidateHashes {
121 alg: Algorithm,
122 hashes: Vec<CandidateHash>,
123 source: VerificationSource,
124 }
125
126 /// Summary of an atetmpt to match the calculated digest against candidates
127 #[derive(PartialEq)]
128 pub enum MatchLevel {
129 Ok,
130 Maybe,
131 Fail,
132 }
133
134 /// The severity of any informational messages to be printed before the final result
135 pub enum MessageLevel {
136 Error,
137 Warning,
138 Note,
139 }
140
141 /// Overall details of an attempt to match the calculated digest against candidates
142 pub struct Verification<'a> {
143 match_level: MatchLevel,
144 comparison_hash: Option<&'a CandidateHash>,
145 messages: Vec<(MessageLevel, String)>,
146 }
147
148 /// Entry point - run the program and handle errors ourselves cleanly.
149 ///
150 /// At the moment there aren't really any errors that can be handled by the application. Therefore
151 /// stringly-typed errors are used and they are all captured here, where the problem is printed
152 /// and the application terminates with a non-zero return code.
153 fn main() {
154 hashgood().unwrap_or_else(|e| {
155 eprintln!("Error: {}", e);
156 process::exit(EXIT_ERR);
157 });
158 }
159
160 /// Main application logic
161 fn hashgood() -> Result<(), Box<dyn Error>> {
162 let opt = get_verified_options()?;
163 let candidates = verify::get_candidate_hashes(&opt)?;
164 let input = calculate::get_input_reader(opt.input.as_path())?;
165 if let Some(c) = candidates {
166 // If we have a candidate hash of a particular type, use that specific algorithm
167 let hashes = calculate::create_digests(&[c.alg], input)?;
168 for (alg, bytes) in hashes {
169 // Should always be true
170 if c.alg == alg {
171 let hash = Hash::new(alg, bytes, &opt.input);
172 let verification = verify::verify_hash(&hash, &c);
173 let successful_match = verification.match_level == MatchLevel::Ok;
174 display::print_hash(
175 &hash,
176 verification.comparison_hash,
177 Some(&c.source),
178 opt.no_colour,
179 )?;
180 display::print_messages(verification.messages, opt.no_colour)?;
181 display::print_match_level(verification.match_level, opt.no_colour)?;
182 if !successful_match {
183 process::exit(EXIT_MISMATCH);
184 }
185 }
186 }
187 } else {
188 // If no candidate, calculate all three common digest types for output
189 let hashes = calculate::create_digests(
190 &[Algorithm::Md5, Algorithm::Sha1, Algorithm::Sha256],
191 input,
192 )?;
193 for (alg, bytes) in hashes {
194 let hash = Hash {
195 alg,
196 bytes,
197 filename: opt.input.file_name().unwrap().to_string_lossy().to_string(),
198 };
199 display::print_hash(&hash, None, None, opt.no_colour)?;
200 }
201 }
202 Ok(())
203 }
204
205 /// Parse the command line options and check for ambiguous or inconsistent settings
206 fn get_verified_options() -> Result<Opt, String> {
207 let opt = Opt::from_args();
208 let hash_methods =
209 opt.hash.is_some() as i32 + opt.get_paste() as i32 + opt.hash_file.is_some() as i32;
210 if hash_methods > 1 {
211 if opt.hash.is_some() {
212 eprintln!("* specified as command line argument");
213 }
214 if opt.get_paste() {
215 eprintln!("* paste from clipboard (-p)")
216 }
217 if opt.hash_file.is_some() {
218 eprintln!("* check hash from file (-c)")
219 }
220 return Err("Error: Hashes were provided by multiple methods. Use only one.".to_owned());
221 }
222 if opt.input.to_str() == Some("-")
223 && opt.hash_file.as_ref().and_then(|h| h.to_str()) == Some("-")
224 {
225 return Err("Error: Cannot use use stdin for both hash file and input data".to_owned());
226 }
227 Ok(opt)
228 }