2 Algorithm
, CandidateHash
, CandidateHashes
, Hash
, MatchLevel
, MessageLevel
, Opt
, Verification
,
5 use clipboard
::ClipboardContext
;
6 use clipboard
::ClipboardProvider
;
10 use std
::io
::prelude
::*;
11 use std
::io
::BufReader
;
12 use std
::path
::PathBuf
;
14 /// Calculate a list of candidate hashes based on the options specified.
15 /// If no hash options have been specified returns None.
16 /// It is assumed to be verified previously that at most one mode has been specified.
17 pub fn get_candidate_hashes(opt
: &Opt
) -> Result
<Option
<CandidateHashes
>, String
> {
18 if let Some(hash_string
) = &opt
.hash
{
19 return Ok(Some(get_by_parameter(hash_string
)?
));
21 return Ok(Some(get_from_clipboard()?
));
22 } else if let Some(hash_file
) = &opt
.hash_file
{
23 return Ok(Some(get_from_file(hash_file
)?
));
28 /// Generate a candidate hash from the provided command line parameter, or throw an error.
29 fn get_by_parameter(param
: &str) -> Result
<CandidateHashes
, String
> {
31 hex
::decode(¶m
).map_err(|_
| "Provided hash is invalid or truncated hex".to_owned())?
;
32 let alg
= Algorithm
::from_len(bytes
.len())?
;
33 let candidate
= CandidateHash
{
39 hashes
: vec
![candidate
],
40 source
: VerificationSource
::CommandArgument
,
44 /// Generate a candidate hash from the system clipboard, or throw an error.
45 fn get_from_clipboard() -> Result
<CandidateHashes
, String
> {
46 let mut ctx
: ClipboardContext
= match ClipboardProvider
::new() {
48 Err(e
) => return Err(format
!("Error getting system clipboard: {}", e
)),
51 let possible_hash
= match ctx
.get_contents() {
53 Err(e
) => format
!("Error reading from clipboard: {}", e
),
56 let bytes
= hex
::decode(&possible_hash
)
57 .map_err(|_
| "Clipboard contains invalid or truncated hex".to_owned())?
;
58 let alg
= Algorithm
::from_len(bytes
.len())?
;
59 let candidate
= CandidateHash
{
65 hashes
: vec
![candidate
],
66 source
: VerificationSource
::Clipboard
,
70 /// Generate a candidate hash from the digests file specified (could be "-" for STDIN), or throw an error.
71 fn get_from_file(path
: &PathBuf
) -> Result
<CandidateHashes
, String
> {
72 // Get a reader for either standard input or the chosen path
73 let reader
: Box
<dyn Read
> = if path
.to_str() == Some("-") {
74 Box
::new(std
::io
::stdin())
76 Box
::new(File
::open(path
).map_err(|_
| {
78 "Unable to open check file at path '{}'",
79 path
.to_string_lossy()
84 // Read the first line, trimmed
85 let mut reader
= BufReader
::new(reader
);
86 let mut line
= String
::new();
89 .map_err(|_
| "Error reading from check file".to_owned())?
;
90 let line
= line
.trim().to_owned();
92 // Does our first line look like a raw hash on its own? If so, use that
93 if let Some(candidate
) = read_raw_candidate_from_file(&line
, &path
) {
97 // Maybe it's a digests file
98 // Reconstruct the full iterator by joining our already-read line with the others
99 let full_lines
= vec
![Ok(line
)].into
_iter
().chain(reader
.lines());
101 // Does the entire file look like a coreutils-style digests file? (SHA1SUMS, etc.)
102 if let Some(candidate
) = read_coreutils_digests_from_file(full_lines
, &path
) {
103 return Ok(candidate
);
106 // If neither of these techniques worked this is a fatal error
107 // The user requested we use this input but we couldn't
109 "Provided check file '{}' was neither a hash nor a valid digests file",
110 path
.to_string_lossy()
114 fn read_raw_candidate_from_file(line
: &str, path
: &PathBuf
) -> Option
<CandidateHashes
> {
115 // It is a little sad to use a dynamic regex in an otherwise nice Rust program
116 // These deserve to be replaced with a good old fashioned static parser
117 // But let's be honest: the impact is negligible
118 let re
= Regex
::new(r
"^([[:xdigit:]]{32}|[[:xdigit:]]{40}|[[:xdigit:]]{64})$").unwrap
();
119 if re
.is
_match
(line
) {
120 // These should both always succeed due to the matching
121 let bytes
= match hex
::decode(line
) {
125 let alg
= match Algorithm
::from_len(bytes
.len()) {
129 return Some(CandidateHashes
{
131 source
: VerificationSource
::RawFile(path
.clone()),
132 hashes
: vec
![CandidateHash
{
141 fn read_coreutils_digests_from_file
<I
>(lines
: I
, path
: &PathBuf
) -> Option
<CandidateHashes
>
143 I
: Iterator
<Item
= io
::Result
<String
>>,
146 r
"^(?P<hash>([[:xdigit:]]{32}|[[:xdigit:]]{40}|[[:xdigit:]]{64})) .(?P<filename>.+)$",
150 let mut hashes
= vec
![];
151 let mut alg
: Option
<Algorithm
> = None
;
155 // Allow (ignore) blank lines
159 // If we can capture a valid line, use it
160 if let Some(captures
) = re
.captures(&l
) {
161 let hash
= &captures
["hash"];
162 let filename
= &captures
["filename"];
163 // Decode the hex and algorithm for this line
164 let line_bytes
= match hex
::decode(hash
) {
168 let line_alg
= match Algorithm
::from_len(line_bytes
.len()) {
172 if alg
.is
_some
() && alg
!= Some(line_alg
) {
173 // Different algorithms in the same digest file are not supported
176 // If we are the first line, we define the overall algorithm
177 alg
= Some(line_alg
);
179 // So far so good - create an entry for this line
180 hashes
.push(CandidateHash
{
182 filename
: Some(filename
.to_owned()),
185 // But if we have a line with content we cannot parse, this is an error
191 // It is a failure if we got zero hashes or we somehow don't know the algorithm
192 if hashes
.is
_empty
() {
195 let alg
= match alg
{
200 // Otherwise all is well and we can return our results
201 Some(CandidateHashes
{
203 source
: VerificationSource
::DigestsFile(path
.clone()),
208 /// Determine if the calculated hash matches any of the candidates.
210 /// Ok result: the hash matches, and if the candidate has a filename, that matches too
211 /// Maybe result: the hash matches but the filename does not
212 /// Fail result: neither of the above
213 pub fn verify_hash
<'a
>(calculated
: &Hash
, candidates
: &'a CandidateHashes
) -> Verification
<'a
> {
214 let mut ok
: Option
<&CandidateHash
> = None
;
215 let mut maybe
: Option
<&CandidateHash
> = None
;
216 let mut messages
= Vec
::new();
218 for candidate
in &candidates
.hashes
{
219 if candidate
.bytes
== calculated
.bytes
{
220 match candidate
.filename
{
221 None
=> ok
= Some(candidate
),
222 Some(ref candidate_filename
) if candidate_filename
== &calculated
.filename
=> {
225 Some(ref candidate_filename
) => {
227 MessageLevel
::Warning
,
229 "The matched hash has filename '{}', which does not match the input.",
233 maybe
= Some(candidate
);
239 // Warn that a "successful" MD5 result is not necessarily great
240 if candidates
.alg
== Algorithm
::Md5
&& (ok
.is
_some
() || maybe
.is
_some
()) {
243 "MD5 can easily be forged. Use a stronger algorithm if possible.".to_owned(),
247 // If we got a full match, great
249 return Verification
{
250 match_level
: MatchLevel
::Ok
,
256 // Second priority, a "maybe" result
258 return Verification
{
259 match_level
: MatchLevel
::Maybe
,
260 comparison_hash
: maybe
,
265 // Otherwise we failed
266 // If we only had one candidate hash, include it
267 let comparison
= match candidates
.hashes
.len() {
268 1 => Some(&candidates
.hashes
[0]),
272 match_level
: MatchLevel
::Fail
,
273 comparison_hash
: comparison
,