use std::fmt;
use std::io::{BufRead, BufReader, Read};
use std::str::FromStr;
use thiserror::Error;
pub type DigestResult<T> = std::result::Result<T, DigestError>;
#[derive(Debug, Error)]
pub enum DigestError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("unsupported digest: {0}")]
Unsupported(String),
}
impl PartialEq for DigestError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(DigestError::Io(e1), DigestError::Io(e2)) => {
e1.kind() == e2.kind()
}
(DigestError::Unsupported(e1), DigestError::Unsupported(e2)) => {
e1 == e2
}
_ => false,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Digest {
BLAKE2s,
MD5,
RMD160,
SHA1,
SHA256,
SHA512,
}
fn hash_file_internal<R: Read, D: digest::Digest + std::io::Write>(
reader: &mut R,
) -> DigestResult<String> {
let mut hasher = D::new();
std::io::copy(reader, &mut hasher)?;
let hash = hasher
.finalize()
.iter()
.fold(String::new(), |mut output, b| {
output.push_str(&format!("{b:02x}"));
output
});
Ok(hash)
}
fn hash_patch_internal<R: Read, D: digest::Digest + std::io::Write>(
reader: &mut R,
) -> DigestResult<String> {
let mut hasher = D::new();
let bufreader = BufReader::new(reader);
for line in bufreader.split(b'\n') {
let line = line?;
if line.windows(7).any(|window| window == b"$NetBSD") {
continue;
}
hasher.update(&line);
hasher.update(b"\n");
}
let hash = hasher
.finalize()
.iter()
.fold(String::new(), |mut output, b| {
output.push_str(&format!("{b:02x}"));
output
});
Ok(hash)
}
fn hash_str_internal<D: digest::Digest + std::io::Write>(
s: &str,
) -> DigestResult<String> {
let mut hasher = D::new();
hasher.update(s);
let hash = hasher
.finalize()
.iter()
.fold(String::new(), |mut output, b| {
output.push_str(&format!("{b:02x}"));
output
});
Ok(hash)
}
impl Digest {
pub fn hash_file<R: Read>(&self, reader: &mut R) -> DigestResult<String> {
match self {
Digest::BLAKE2s => {
hash_file_internal::<_, blake2::Blake2s256>(reader)
}
Digest::MD5 => hash_file_internal::<_, md5::Md5>(reader),
Digest::RMD160 => {
hash_file_internal::<_, ripemd::Ripemd160>(reader)
}
Digest::SHA1 => hash_file_internal::<_, sha1::Sha1>(reader),
Digest::SHA256 => hash_file_internal::<_, sha2::Sha256>(reader),
Digest::SHA512 => hash_file_internal::<_, sha2::Sha512>(reader),
}
}
pub fn hash_patch<R: Read>(&self, reader: &mut R) -> DigestResult<String> {
match self {
Digest::BLAKE2s => {
hash_patch_internal::<_, blake2::Blake2s256>(reader)
}
Digest::MD5 => hash_patch_internal::<_, md5::Md5>(reader),
Digest::RMD160 => {
hash_patch_internal::<_, ripemd::Ripemd160>(reader)
}
Digest::SHA1 => hash_patch_internal::<_, sha1::Sha1>(reader),
Digest::SHA256 => hash_patch_internal::<_, sha2::Sha256>(reader),
Digest::SHA512 => hash_patch_internal::<_, sha2::Sha512>(reader),
}
}
pub fn hash_str(&self, s: &str) -> DigestResult<String> {
match self {
Digest::BLAKE2s => hash_str_internal::<blake2::Blake2s256>(s),
Digest::MD5 => hash_str_internal::<md5::Md5>(s),
Digest::RMD160 => hash_str_internal::<ripemd::Ripemd160>(s),
Digest::SHA1 => hash_str_internal::<sha1::Sha1>(s),
Digest::SHA256 => hash_str_internal::<sha2::Sha256>(s),
Digest::SHA512 => hash_str_internal::<sha2::Sha512>(s),
}
}
}
impl FromStr for Digest {
type Err = DigestError;
fn from_str(s: &str) -> DigestResult<Self> {
match s.to_lowercase().as_str() {
"blake2s" => Ok(Digest::BLAKE2s),
"md5" => Ok(Digest::MD5),
"rmd160" => Ok(Digest::RMD160),
"sha1" => Ok(Digest::SHA1),
"sha256" => Ok(Digest::SHA256),
"sha512" => Ok(Digest::SHA512),
_ => Err(DigestError::Unsupported(s.to_string())),
}
}
}
impl fmt::Display for Digest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Digest::BLAKE2s => write!(f, "BLAKE2s"),
Digest::MD5 => write!(f, "MD5"),
Digest::RMD160 => write!(f, "RMD160"),
Digest::SHA1 => write!(f, "SHA1"),
Digest::SHA256 => write!(f, "SHA256"),
Digest::SHA512 => write!(f, "SHA512"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digest_invalid() -> DigestResult<()> {
let moo = String::from("moo");
let d = Digest::from_str(&moo);
assert_eq!(d, Err(DigestError::Unsupported(moo)));
Ok(())
}
#[test]
fn digest_str() -> DigestResult<()> {
let d = Digest::from_str("SHA1")?;
let h = d.hash_str("hello there")?;
assert_eq!(h, "6e71b3cac15d32fe2d36c270887df9479c25c640");
Ok(())
}
#[test]
fn digest_str_lower() -> DigestResult<()> {
let d = Digest::from_str("sha1")?;
let h = d.hash_str("hello there")?;
assert_eq!(h, "6e71b3cac15d32fe2d36c270887df9479c25c640");
Ok(())
}
}