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