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