2     Algorithm
, CandidateHash
, CandidateHashes
, Hash
, MatchLevel
, MessageLevel
, Opt
, Verification
, 
   5 #[cfg(feature = "paste")] 
   6 use copypasta
::{ClipboardContext
, ClipboardProvider
}; 
   9 use std
::io
::prelude
::*; 
  10 use std
::io
::BufReader
; 
  13 /// Calculate a list of candidate hashes based on the options specified. 
  14 /// If no hash options have been specified returns None. 
  15 /// It is assumed to be verified previously that at most one mode has been specified. 
  16 pub fn get_candidate_hashes(opt
: &Opt
) -> Result
<Option
<CandidateHashes
>, String
> { 
  17     if let Some(hash_string
) = &opt
.hash 
{ 
  18         return Ok(Some(get_by_parameter(hash_string
)?
)); 
  19     } else if opt
.get_paste() { 
  20         return Ok(Some(get_from_clipboard()?
)); 
  21     } else if let Some(hash_file
) = &opt
.hash_file 
{ 
  22         return Ok(Some(get_from_file(hash_file
)?
)); 
  27 /// Generate a candidate hash from the provided command line parameter, or throw an error. 
  28 fn get_by_parameter(param
: &str) -> Result
<CandidateHashes
, String
> { 
  30         hex
::decode(¶m
).map_err(|_
| "Provided hash is invalid or truncated hex".to_owned())?
; 
  31     let alg 
= Algorithm
::from_len(bytes
.len())?
; 
  32     let candidate 
= CandidateHash 
{ 
  38         hashes
: vec
![candidate
], 
  39         source
: VerificationSource
::CommandArgument
, 
  43 /// Generate a candidate hash from the system clipboard, or throw an error. 
  44 fn get_from_clipboard() -> Result
<CandidateHashes
, String
> { 
  45     #[cfg(feature = "paste")] 
  47         let mut ctx
: ClipboardContext 
= match ClipboardContext
::new() { 
  49             Err(e
) => return Err(format
!("Error getting system clipboard: {}", e
)), 
  52         let possible_hash 
= match ctx
.get_contents() { 
  54             Err(e
) => format
!("Error reading from clipboard: {}", e
), 
  57         let bytes 
= hex
::decode(&possible_hash
) 
  58             .map_err(|_
| "Clipboard contains invalid or truncated hex".to_owned())?
; 
  59         let alg 
= Algorithm
::from_len(bytes
.len())?
; 
  60         let candidate 
= CandidateHash 
{ 
  66             hashes
: vec
![candidate
], 
  67             source
: VerificationSource
::Clipboard
, 
  70     #[cfg(not(feature = "paste"))] 
  72         Err("Paste not implemented".to_owned()) 
  76 /// Generate a candidate hash from the digests file specified (could be "-" for STDIN), or throw an error. 
  77 fn get_from_file(path
: &Path
) -> Result
<CandidateHashes
, String
> { 
  78     // Get a reader for either standard input or the chosen path 
  79     let reader
: Box
<dyn Read
> = if path
.to_str() == Some("-") { 
  80         Box
::new(std
::io
::stdin()) 
  82         Box
::new(File
::open(path
).map_err(|_
| { 
  84                 "Unable to open check file at path '{}'", 
  85                 path
.to_string_lossy() 
  90     // Read the first line, trimmed 
  91     let mut reader 
= BufReader
::new(reader
); 
  92     let mut line 
= String
::new(); 
  95         .map_err(|_
| "Error reading from check file".to_owned())?
; 
  96     let line 
= line
.trim().to_owned(); 
  98     // Does our first line look like a raw hash on its own? If so, use that 
  99     if let Some(candidate
) = read_raw_candidate_from_file(&line
, path
) { 
 100         return Ok(candidate
); 
 103     // Maybe it's a digests file 
 104     // Reconstruct the full iterator by joining our already-read line with the others 
 105     let full_lines 
= vec
![Ok(line
)].into
_iter
().chain(reader
.lines()); 
 107     // Does the entire file look like a coreutils-style digests file? (SHA1SUMS, etc.) 
 108     if let Some(candidate
) = read_coreutils_digests_from_file(full_lines
, path
) { 
 109         return Ok(candidate
); 
 112     // If neither of these techniques worked this is a fatal error 
 113     // The user requested we use this input but we couldn't 
 115         "Provided check file '{}' was neither a hash nor a valid digests file", 
 116         path
.to_string_lossy() 
 120 fn try_parse_hash(s
: &str) -> Option
<(Algorithm
, Vec
<u8>)> { 
 121     let bytes 
= match hex
::decode(s
.trim()) { 
 125     let alg 
= match Algorithm
::from_len(bytes
.len()) { 
 132 fn read_raw_candidate_from_file(line
: &str, path
: &Path
) -> Option
<CandidateHashes
> { 
 133     let (alg
, bytes
) = try_parse_hash(line
)?
; 
 134     Some(CandidateHashes 
{ 
 136         source
: VerificationSource
::RawFile(path
.to_string_lossy().to_string()), 
 137         hashes
: vec
![CandidateHash 
{ 
 144 fn read_coreutils_digests_from_file
<I
, S
>(lines
: I
, path
: &Path
) -> Option
<CandidateHashes
> 
 146     I
: Iterator
<Item 
= io
::Result
<S
>>, 
 149     let mut hashes 
= vec
![]; 
 150     let mut alg
: Option
<Algorithm
> = None
; 
 151     for l 
in lines
.flatten
() { 
 152         let l 
= l
.as_ref().trim(); 
 153         // Allow (ignore) blank lines 
 158         // <valid-hash><space><space-or-*><filename> 
 159         let (line_alg
, bytes
, filename
) = match l
 
 161             .and_then(|space_pos
| { 
 162                 // Char before filename should be space for text or * for binary 
 163                 match l
.chars().nth(space_pos 
+ 1) { 
 164                     Some(' '
) | Some('
*'
) => (l
.get(..space_pos
)).zip(l
.get(space_pos 
+ 2..)), 
 168             .and_then(|(maybe_hash
, filename
)| { 
 169                 // Filename should be in this position without extra whitespace 
 170                 if filename
.trim() == filename 
{ 
 171                     try_parse_hash(maybe_hash
).map(|(alg
, bytes
)| (alg
, bytes
, filename
)) 
 178                 // if we have a line with content we cannot parse, this is an error 
 182         if alg
.is
_some
() && alg 
!= Some(line_alg
) { 
 183             // Different algorithms in the same digest file are not supported 
 186             // If we are the first line, we define the overall algorithm 
 187             alg 
= Some(line_alg
); 
 189         // So far so good - create an entry for this line 
 190         hashes
.push(CandidateHash 
{ 
 192             filename
: Some(filename
.to_owned()), 
 196     // It is a failure if we got zero hashes or we somehow don't know the algorithm 
 197     if hashes
.is
_empty
() { 
 200     let alg 
= match alg 
{ 
 205     // Otherwise all is well and we can return our results 
 206     Some(CandidateHashes 
{ 
 208         source
: VerificationSource
::DigestsFile(path
.to_string_lossy().to_string()), 
 213 /// Determine if the calculated hash matches any of the candidates. 
 215 /// Ok result: the hash matches, and if the candidate has a filename, that matches too 
 216 /// Maybe result: the hash matches but the filename does not 
 217 /// Fail result: neither of the above 
 218 pub fn verify_hash
<'a
>(calculated
: &Hash
, candidates
: &'a CandidateHashes
) -> Verification
<'a
> { 
 219     let mut ok
: Option
<&CandidateHash
> = None
; 
 220     let mut maybe
: Option
<&CandidateHash
> = None
; 
 221     let mut messages 
= Vec
::new(); 
 223     for candidate 
in &candidates
.hashes 
{ 
 224         if candidate
.bytes 
== calculated
.bytes 
{ 
 225             match candidate
.filename 
{ 
 226                 None 
=> ok 
= Some(candidate
), 
 227                 Some(ref candidate_filename
) if candidate_filename 
== &calculated
.filename 
=> { 
 230                 Some(ref candidate_filename
) => { 
 232                         MessageLevel
::Warning
, 
 234                             "The matched hash has filename '{}', which does not match the input.", 
 238                     maybe 
= Some(candidate
); 
 244     // Warn that a "successful" MD5 result is not necessarily great 
 245     if candidates
.alg 
== Algorithm
::Md5 
&& (ok
.is
_some
() || maybe
.is
_some
()) { 
 248             "MD5 can easily be forged. Use a stronger algorithm if possible.".to_owned(), 
 252     // If we got a full match, great 
 254         return Verification 
{ 
 255             match_level
: MatchLevel
::Ok
, 
 261     // Second priority, a "maybe" result 
 263         return Verification 
{ 
 264             match_level
: MatchLevel
::Maybe
, 
 265             comparison_hash
: maybe
, 
 270     // Otherwise we failed 
 271     // If we only had one candidate hash, include it 
 272     let comparison 
= match candidates
.hashes
.len() { 
 273         1 => Some(&candidates
.hashes
[0]), 
 277         match_level
: MatchLevel
::Fail
, 
 278         comparison_hash
: comparison
, 
 288     fn test_read_raw_inputs() { 
 289         let example_path 
= Path
::new("some_file"); 
 290         let valid_md5 
= "d229da563da18fe5d58cd95a6467d584"; 
 291         let valid_sha1 
= "b314c7ebb7d599944981908b7f3ed33a30e78f3a"; 
 292         let valid_sha1_2 
= valid_sha1
.to_uppercase(); 
 293         let valid_sha256 
= "1eb85fc97224598dad1852b5d6483bbcf0aa8608790dcc657a5a2a761ae9c8c6"; 
 297         let invalid3 
= "d229da563da18fe5d58cd95a6467d58"; 
 298         let invalid4 
= "1eb85fc97224598dad1852b5d6483bbcf0aa8608790dcc657a5a2a761ae9c8c67"; 
 299         let invalid5 
= "1eb85fc97224598dad1852b5d 483bbcf0aa8608790dcc657a5a2a761ae9c8c6"; 
 302             read_raw_candidate_from_file(valid_md5
, example_path
), 
 303             Some(CandidateHashes 
{ 
 309             read_raw_candidate_from_file(valid_sha1
, example_path
), 
 310             Some(CandidateHashes 
{ 
 311                 alg
: Algorithm
::Sha1
, 
 316             read_raw_candidate_from_file(&valid_sha1_2
, example_path
), 
 317             Some(CandidateHashes 
{ 
 318                 alg
: Algorithm
::Sha1
, 
 323             read_raw_candidate_from_file(valid_sha256
, example_path
), 
 324             Some(CandidateHashes 
{ 
 325                 alg
: Algorithm
::Sha256
, 
 330         for i 
in &[invalid1
, invalid2
, invalid3
, invalid4
, invalid5
] { 
 331             assert
!(read_raw_candidate_from_file(*i
, example_path
).is
_none
()); 
 336     fn test_read_shasums() { 
 337         let shasums 
= "4b91f7a387a6edd4a7c0afb2897f1ca968c9695b *cp 
 338         75eb7420a9f5a260b04a3e8ad51e50f2838a17fc  lel.txt 
 340         fe6c26d485a3573a1cb0ad0682f5105325a1905f  shasums"; 
 341         let lines 
= shasums
.lines().map(std
::io
::Result
::Ok
); 
 342         let path 
= Path
::new("SHASUMS"); 
 343         let candidates 
= read_coreutils_digests_from_file(lines
, path
); 
 347             Some(CandidateHashes 
{ 
 348                 alg
: Algorithm
::Sha1
, 
 351                         bytes
: hex
::decode("4b91f7a387a6edd4a7c0afb2897f1ca968c9695b").unwrap
(), 
 352                         filename
: Some("cp".to_owned()), 
 355                         bytes
: hex
::decode("75eb7420a9f5a260b04a3e8ad51e50f2838a17fc").unwrap
(), 
 356                         filename
: Some("lel.txt".to_owned()), 
 359                         bytes
: hex
::decode("fe6c26d485a3573a1cb0ad0682f5105325a1905f").unwrap
(), 
 360                         filename
: Some("shasums".to_owned()), 
 363                 source
: VerificationSource
::DigestsFile(path
.to_string_lossy().to_string()), 
 369     fn test_invalid_shasums() { 
 370         let no_format 
= "4b91f7a387a6edd4a7c0afb2897f1ca968c9695b cp"; 
 371         let invalid_format 
= "4b91f7a387a6edd4a7c0afb2897f1ca968c9695b .cp"; 
 372         let extra_space 
= "4b91f7a387a6edd4a7c0afb2897f1ca968c9695b   cp"; 
 374         for digest 
in [no_format
, invalid_format
, extra_space
] { 
 375             let lines 
= digest
.lines().map(std
::io
::Result
::Ok
); 
 377                 read_coreutils_digests_from_file(lines
, Path
::new("SHASUMS")).is
_none
(), 
 378                 "Should be invalid digest: {:?}",