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