use blake2::Blake2b512;
use digest::Digest;
use sha2::{Sha256, Sha512};
use crate::json::{repr_list, repr_str};
use crate::{Error, Result};
pub const DEFAULT_ALGO: &str = "sha256";
pub const DIGEST_ALGOS: [&str; 3] = ["sha256", "sha512", "blake2b"];
pub enum Hasher {
Sha256(Sha256),
Sha512(Sha512),
Blake2b(Blake2b512),
}
impl Hasher {
pub fn new(algo: &str) -> Result<Self> {
match algo {
"sha256" => Ok(Hasher::Sha256(Sha256::new())),
"sha512" => Ok(Hasher::Sha512(Sha512::new())),
"blake2b" => Ok(Hasher::Blake2b(Blake2b512::new())),
other => Err(Error::coded(
"E703",
format!(
"unsupported digest algorithm {}; expected one of {}",
repr_str(other),
repr_list(&DIGEST_ALGOS)
),
)),
}
}
pub fn update(&mut self, bytes: &[u8]) {
match self {
Hasher::Sha256(h) => h.update(bytes),
Hasher::Sha512(h) => h.update(bytes),
Hasher::Blake2b(h) => h.update(bytes),
}
}
pub fn algo(&self) -> &'static str {
match self {
Hasher::Sha256(_) => "sha256",
Hasher::Sha512(_) => "sha512",
Hasher::Blake2b(_) => "blake2b",
}
}
pub fn hexdigest(self) -> String {
match self {
Hasher::Sha256(h) => hex::encode(h.finalize()),
Hasher::Sha512(h) => hex::encode(h.finalize()),
Hasher::Blake2b(h) => hex::encode(h.finalize()),
}
}
pub fn finish(self) -> String {
let algo = self.algo();
format!("{algo}:{}", self.hexdigest())
}
}
pub fn hash_hex(algo: &str, payload: &[u8]) -> Result<String> {
let mut h = Hasher::new(algo)?;
h.update(payload);
Ok(h.hexdigest())
}
pub fn digest_bytes(payload: &[u8], algo: &str) -> Result<String> {
let mut h = Hasher::new(algo)?;
h.update(payload);
Ok(h.finish())
}
pub fn parse_digest(value: &str) -> Result<(String, String)> {
let malformed = || Error::coded("E703", format!("malformed digest {}", repr_str(value)));
let (algo, hexdigest) = value.split_once(':').ok_or_else(malformed)?;
if hexdigest.is_empty() || !DIGEST_ALGOS.contains(&algo) {
return Err(malformed());
}
let body = hexdigest.trim();
let body = body.strip_prefix(['+', '-']).unwrap_or(body);
let body = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")).unwrap_or(body);
if body.is_empty()
|| body.starts_with('_')
|| body.ends_with('_')
|| body.contains("__")
|| !body.chars().all(|c| c.is_ascii_hexdigit() || c == '_')
{
return Err(malformed());
}
Ok((algo.to_string(), hexdigest.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digests() {
assert_eq!(
digest_bytes(b"abc", "sha256").unwrap(),
"sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_eq!(hash_hex("blake2b", b"").unwrap().len(), 128);
assert_eq!(Hasher::new("blake3").err().unwrap().code(), Some("E703"));
assert!(parse_digest("sha256:00ff").is_ok());
assert!(parse_digest("sha256:").is_err());
assert!(parse_digest("md5:00").is_err());
assert!(parse_digest("sha256:xyz").is_err());
}
}