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