]> code.octet-stream.net Git - hashgood/blob - src/main.rs
Give more friendly error messages if path is not a regular file
[hashgood] / src / main.rs
1 use std::error::Error;
2 use std::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 pub enum VerificationSource {
75 CommandArgument,
76 Clipboard,
77 RawFile(PathBuf),
78 DigestsFile(PathBuf),
79 }
80
81 /// A complete standalone hash result
82 pub struct Hash {
83 alg: Algorithm,
84 bytes: Vec<u8>,
85 filename: String,
86 }
87
88 impl Hash {
89 pub fn new(alg: Algorithm, bytes: Vec<u8>, path: &PathBuf) -> Self {
90 // Taking the filename component should always work?
91 // If not, just fall back to the full path
92 let filename = match path.file_name() {
93 Some(filename) => filename.to_string_lossy(),
94 None => path.to_string_lossy(),
95 };
96 Self {
97 alg,
98 bytes,
99 filename: filename.to_string(),
100 }
101 }
102 }
103
104 /// A possible hash to match against. The algorithm is assumed.
105 pub struct CandidateHash {
106 bytes: Vec<u8>,
107 filename: Option<String>,
108 }
109
110 /// A list of candidate hashes that our input could potentially match. At this point it is
111 /// assumed that we will be verifying a digest of a particular, single algorithm.
112 pub struct CandidateHashes {
113 alg: Algorithm,
114 hashes: Vec<CandidateHash>,
115 source: VerificationSource,
116 }
117
118 /// Summary of an atetmpt to match the calculated digest against candidates
119 pub enum MatchLevel {
120 Ok,
121 Maybe,
122 Fail,
123 }
124
125 /// The severity of any informational messages to be printed before the final result
126 pub enum MessageLevel {
127 Error,
128 Warning,
129 Note,
130 }
131
132 /// Overall details of an attempt to match the calculated digest against candidates
133 pub struct Verification<'a> {
134 match_level: MatchLevel,
135 comparison_hash: Option<&'a CandidateHash>,
136 messages: Vec<(MessageLevel, String)>,
137 }
138
139 /// Entry point - run the program and handle errors ourselves cleanly.
140 ///
141 /// At the moment there aren't really any errors that can be handled by the application. Therefore
142 /// stringly-typed errors are used and they are all captured here, where the problem is printed
143 /// and the application terminates with a non-zero return code.
144 fn main() {
145 hashgood().unwrap_or_else(|e| {
146 eprintln!("Error: {}", e);
147 process::exit(1);
148 });
149 }
150
151 /// Main application logic
152 fn hashgood() -> Result<(), Box<dyn Error>> {
153 let opt = get_verified_options()?;
154 let candidates = verify::get_candidate_hashes(&opt)?;
155 let input = calculate::get_input_reader(&opt.input)?;
156 if let Some(c) = candidates {
157 // If we have a candidate hash of a particular type, use that specific algorithm
158 let hashes = calculate::create_digests(&[c.alg], input)?;
159 for (alg, bytes) in hashes {
160 // Should always be true
161 if c.alg == alg {
162 let hash = Hash::new(alg, bytes, &opt.input);
163 let verification = verify::verify_hash(&hash, &c);
164 display::print_hash(
165 &hash,
166 verification.comparison_hash,
167 Some(&c.source),
168 opt.no_colour,
169 )?;
170 display::print_messages(verification.messages, opt.no_colour)?;
171 display::print_match_level(verification.match_level, opt.no_colour)?;
172 }
173 }
174 } else {
175 // If no candidate, calculate all three common digest types for output
176 let hashes = calculate::create_digests(
177 &[Algorithm::Md5, Algorithm::Sha1, Algorithm::Sha256],
178 input,
179 )?;
180 for (alg, bytes) in hashes {
181 let hash = Hash {
182 alg,
183 bytes,
184 filename: opt.input.file_name().unwrap().to_string_lossy().to_string(),
185 };
186 display::print_hash(&hash, None, None, opt.no_colour)?;
187 }
188 }
189 Ok(())
190 }
191
192 /// Parse the command line options and check for ambiguous or inconsistent settings
193 fn get_verified_options() -> Result<Opt, String> {
194 let opt = Opt::from_args();
195 let hash_methods =
196 opt.hash.is_some() as i32 + opt.get_paste() as i32 + opt.hash_file.is_some() as i32;
197 if hash_methods > 1 {
198 if opt.hash.is_some() {
199 eprintln!("* specified as command line argument");
200 }
201 if opt.get_paste() {
202 eprintln!("* paste from clipboard (-p)")
203 }
204 if opt.hash_file.is_some() {
205 eprintln!("* check hash from file (-c)")
206 }
207 return Err("Error: Hashes were provided by multiple methods. Use only one.".to_owned());
208 }
209 if opt.input.to_str() == Some("-")
210 && opt.hash_file.as_ref().and_then(|h| h.to_str()) == Some("-")
211 {
212 return Err("Error: Cannot use use stdin for both hash file and input data".to_owned());
213 }
214 Ok(opt)
215 }