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