2 Algorithm
, CandidateHash
, CandidateHashes
, Hash
, MatchLevel
, MessageLevel
, Opt
, Verification
,
7 use std
::io
::prelude
::*;
8 use std
::io
::BufReader
;
11 /// Calculate a list of candidate hashes based on the options specified.
12 /// If no hash options have been specified returns None.
13 /// It is assumed to be verified previously that at most one mode has been specified.
14 pub fn get_candidate_hashes(opt
: &Opt
) -> Result
<Option
<CandidateHashes
>, String
> {
15 if let Some(hash_string
) = &opt
.hash
{
16 return Ok(Some(get_by_parameter(hash_string
)?
));
17 } else if let Some(hash_file
) = &opt
.hash_file
{
18 return Ok(Some(get_from_file(hash_file
)?
));
23 /// Generate a candidate hash from the provided command line parameter, or throw an error.
24 fn get_by_parameter(param
: &str) -> Result
<CandidateHashes
, String
> {
26 hex
::decode(¶m
).map_err(|_
| "Provided hash is invalid or truncated hex".to_owned())?
;
27 let alg
= Algorithm
::from_len(bytes
.len())?
;
28 let candidate
= CandidateHash
{
34 hashes
: vec
![candidate
],
35 source
: VerificationSource
::CommandArgument
,
39 /// Generate a candidate hash from the digests file specified (could be "-" for STDIN), or throw an error.
40 fn get_from_file(path
: &Path
) -> Result
<CandidateHashes
, String
> {
41 // Get a reader for either standard input or the chosen path
42 let reader
: Box
<dyn Read
> = if path
.to_str() == Some("-") {
43 Box
::new(std
::io
::stdin())
45 Box
::new(File
::open(path
).map_err(|_
| {
47 "Unable to open check file at path '{}'",
48 path
.to_string_lossy()
53 // Read the first line, trimmed
54 let mut reader
= BufReader
::new(reader
);
55 let mut line
= String
::new();
58 .map_err(|_
| "Error reading from check file".to_owned())?
;
59 let line
= line
.trim().to_owned();
61 // Does our first line look like a raw hash on its own? If so, use that
62 if let Some(candidate
) = read_raw_candidate_from_file(&line
, path
) {
66 // Maybe it's a digests file
67 // Reconstruct the full iterator by joining our already-read line with the others
68 let full_lines
= vec
![Ok(line
)].into
_iter
().chain(reader
.lines());
70 // Does the entire file look like a coreutils-style digests file? (SHA1SUMS, etc.)
71 if let Some(candidate
) = read_coreutils_digests_from_file(full_lines
, path
) {
75 // If neither of these techniques worked this is a fatal error
76 // The user requested we use this input but we couldn't
78 "Provided check file '{}' was neither a hash nor a valid digests file",
79 path
.to_string_lossy()
83 fn try_parse_hash(s
: &str) -> Option
<(Algorithm
, Vec
<u8>)> {
84 let bytes
= match hex
::decode(s
.trim()) {
88 let alg
= match Algorithm
::from_len(bytes
.len()) {
95 fn read_raw_candidate_from_file(line
: &str, path
: &Path
) -> Option
<CandidateHashes
> {
96 let (alg
, bytes
) = try_parse_hash(line
)?
;
97 Some(CandidateHashes
{
99 source
: VerificationSource
::RawFile(path
.to_string_lossy().to_string()),
100 hashes
: vec
![CandidateHash
{
107 fn read_coreutils_digests_from_file
<I
, S
>(lines
: I
, path
: &Path
) -> Option
<CandidateHashes
>
109 I
: Iterator
<Item
= io
::Result
<S
>>,
112 let mut hashes
= vec
![];
113 let mut alg
: Option
<Algorithm
> = None
;
114 for l
in lines
.flatten
() {
115 let l
= l
.as_ref().trim();
116 // Allow (ignore) blank lines
121 // <valid-hash><space><space-or-*><filename>
122 let (line_alg
, bytes
, filename
) = match l
124 .and_then(|space_pos
| {
125 // Char before filename should be space for text or * for binary
126 match l
.chars().nth(space_pos
+ 1) {
127 Some(' '
) | Some('
*'
) => (l
.get(..space_pos
)).zip(l
.get(space_pos
+ 2..)),
131 .and_then(|(maybe_hash
, filename
)| {
132 // Filename should be in this position without extra whitespace
133 if filename
.trim() == filename
{
134 try_parse_hash(maybe_hash
).map(|(alg
, bytes
)| (alg
, bytes
, filename
))
141 // if we have a line with content we cannot parse, this is an error
145 if alg
.is
_some
() && alg
!= Some(line_alg
) {
146 // Different algorithms in the same digest file are not supported
149 // If we are the first line, we define the overall algorithm
150 alg
= Some(line_alg
);
152 // So far so good - create an entry for this line
153 hashes
.push(CandidateHash
{
155 filename
: Some(filename
.to_owned()),
159 // It is a failure if we got zero hashes or we somehow don't know the algorithm
160 if hashes
.is
_empty
() {
163 let alg
= match alg
{
168 // Otherwise all is well and we can return our results
169 Some(CandidateHashes
{
171 source
: VerificationSource
::DigestsFile(path
.to_string_lossy().to_string()),
176 /// Determine if the calculated hash matches any of the candidates.
178 /// Ok result: the hash matches, and if the candidate has a filename, that matches too
179 /// Maybe result: the hash matches but the filename does not
180 /// Fail result: neither of the above
181 pub fn verify_hash
<'a
>(calculated
: &Hash
, candidates
: &'a CandidateHashes
) -> Verification
<'a
> {
182 let mut ok
: Option
<&CandidateHash
> = None
;
183 let mut maybe
: Option
<&CandidateHash
> = None
;
184 let mut messages
= Vec
::new();
186 for candidate
in &candidates
.hashes
{
187 if candidate
.bytes
== calculated
.bytes
{
188 match candidate
.filename
{
189 None
=> ok
= Some(candidate
),
190 Some(ref candidate_filename
) if candidate_filename
== &calculated
.filename
=> {
193 Some(ref candidate_filename
) => {
195 MessageLevel
::Warning
,
197 "The matched hash has filename '{}', which does not match the input.",
201 maybe
= Some(candidate
);
207 // Warn that a "successful" MD5 result is not necessarily great
208 if candidates
.alg
== Algorithm
::Md5
&& (ok
.is
_some
() || maybe
.is
_some
()) {
211 "MD5 can easily be forged. Use a stronger algorithm if possible.".to_owned(),
215 // If we got a full match, great
217 return Verification
{
218 match_level
: MatchLevel
::Ok
,
224 // Second priority, a "maybe" result
226 return Verification
{
227 match_level
: MatchLevel
::Maybe
,
228 comparison_hash
: maybe
,
233 // Otherwise we failed
234 // If we only had one candidate hash, include it
235 let comparison
= match candidates
.hashes
.len() {
236 1 => Some(&candidates
.hashes
[0]),
240 match_level
: MatchLevel
::Fail
,
241 comparison_hash
: comparison
,
251 fn test_read_raw_inputs() {
252 let example_path
= Path
::new("some_file");
253 let valid_md5
= "d229da563da18fe5d58cd95a6467d584";
254 let valid_sha1
= "b314c7ebb7d599944981908b7f3ed33a30e78f3a";
255 let valid_sha1_2
= valid_sha1
.to_uppercase();
256 let valid_sha256
= "1eb85fc97224598dad1852b5d6483bbcf0aa8608790dcc657a5a2a761ae9c8c6";
260 let invalid3
= "d229da563da18fe5d58cd95a6467d58";
261 let invalid4
= "1eb85fc97224598dad1852b5d6483bbcf0aa8608790dcc657a5a2a761ae9c8c67";
262 let invalid5
= "1eb85fc97224598dad1852b5d 483bbcf0aa8608790dcc657a5a2a761ae9c8c6";
265 read_raw_candidate_from_file(valid_md5
, example_path
),
266 Some(CandidateHashes
{
272 read_raw_candidate_from_file(valid_sha1
, example_path
),
273 Some(CandidateHashes
{
274 alg
: Algorithm
::Sha1
,
279 read_raw_candidate_from_file(&valid_sha1_2
, example_path
),
280 Some(CandidateHashes
{
281 alg
: Algorithm
::Sha1
,
286 read_raw_candidate_from_file(valid_sha256
, example_path
),
287 Some(CandidateHashes
{
288 alg
: Algorithm
::Sha256
,
293 for i
in &[invalid1
, invalid2
, invalid3
, invalid4
, invalid5
] {
294 assert
!(read_raw_candidate_from_file(*i
, example_path
).is
_none
());
299 fn test_read_shasums() {
300 let shasums
= "4b91f7a387a6edd4a7c0afb2897f1ca968c9695b *cp
301 75eb7420a9f5a260b04a3e8ad51e50f2838a17fc lel.txt
303 fe6c26d485a3573a1cb0ad0682f5105325a1905f shasums";
304 let lines
= shasums
.lines().map(std
::io
::Result
::Ok
);
305 let path
= Path
::new("SHASUMS");
306 let candidates
= read_coreutils_digests_from_file(lines
, path
);
310 Some(CandidateHashes
{
311 alg
: Algorithm
::Sha1
,
314 bytes
: hex
::decode("4b91f7a387a6edd4a7c0afb2897f1ca968c9695b").unwrap
(),
315 filename
: Some("cp".to_owned()),
318 bytes
: hex
::decode("75eb7420a9f5a260b04a3e8ad51e50f2838a17fc").unwrap
(),
319 filename
: Some("lel.txt".to_owned()),
322 bytes
: hex
::decode("fe6c26d485a3573a1cb0ad0682f5105325a1905f").unwrap
(),
323 filename
: Some("shasums".to_owned()),
326 source
: VerificationSource
::DigestsFile(path
.to_string_lossy().to_string()),
332 fn test_invalid_shasums() {
333 let no_format
= "4b91f7a387a6edd4a7c0afb2897f1ca968c9695b cp";
334 let invalid_format
= "4b91f7a387a6edd4a7c0afb2897f1ca968c9695b .cp";
335 let extra_space
= "4b91f7a387a6edd4a7c0afb2897f1ca968c9695b cp";
337 for digest
in [no_format
, invalid_format
, extra_space
] {
338 let lines
= digest
.lines().map(std
::io
::Result
::Ok
);
340 read_coreutils_digests_from_file(lines
, Path
::new("SHASUMS")).is
_none
(),
341 "Should be invalid digest: {:?}",