use sha2::{self, Digest, Sha256};
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDigest {
digest: String,
algorithm: DigestAlgorithm,
}
#[derive(Display, Clone, Debug, PartialEq, EnumString)]
pub enum DigestAlgorithm {
#[strum(to_string = "sha256")]
Sha256,
}
#[derive(Debug, thiserror::Error)]
pub enum ContentDigestError {
#[error("digest {0} does not have algorithm prefix")]
BadDigest(String),
#[error("unknown algorithm")]
AlgorithmUnknown(#[from] <DigestAlgorithm as std::str::FromStr>::Err),
#[error("verification failed: expected '{expected}', got '{got}'")]
Verify {
expected: ContentDigest,
got: ContentDigest,
},
}
impl ContentDigest {
pub fn try_new(digest: String) -> std::result::Result<Self, ContentDigestError> {
let digest_split = digest.split(':').collect::<Vec<&str>>();
if digest_split.len() != 2 {
return Err(ContentDigestError::BadDigest(digest));
}
let algorithm = std::str::FromStr::from_str(digest_split[0])?;
Ok(ContentDigest {
digest: digest_split[1].to_string(),
algorithm,
})
}
pub fn start_hash(&self) -> Sha256 {
self.algorithm.new()
}
pub fn try_verify(&self, input: &[u8]) -> std::result::Result<(), ContentDigestError> {
let hash = self.algorithm.hash(input);
let layer_digest = Self::try_new(hash)?;
if self != &layer_digest {
return Err(ContentDigestError::Verify {
expected: self.clone(),
got: layer_digest,
});
}
trace!("content verification succeeded for '{}'", &layer_digest);
Ok(())
}
pub fn try_verify_hash(&self, input: &Sha256) -> std::result::Result<(), ContentDigestError> {
let hash = self.algorithm.finalize(input);
let layer_digest = Self::try_new(hash)?;
if self != &layer_digest {
return Err(ContentDigestError::Verify {
expected: self.clone(),
got: layer_digest,
});
}
trace!("content verification succeeded for '{}'", &layer_digest);
Ok(())
}
}
impl std::fmt::Display for ContentDigest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}:{}", self.algorithm, self.digest)
}
}
impl DigestAlgorithm {
fn hash(&self, input: &[u8]) -> String {
match self {
DigestAlgorithm::Sha256 => {
let hash = sha2::Sha256::digest(input);
format!("{}:{:x}", self, hash)
}
}
}
fn new(&self) -> Sha256 {
match self {
DigestAlgorithm::Sha256 => sha2::Sha256::new(),
}
}
fn finalize(&self, hash: &Sha256) -> String {
match self {
DigestAlgorithm::Sha256 => {
let h = hash.clone().finalize();
format!(
"{}:{}",
self,
h.iter().map(|b| format!("{:02x}", b)).collect::<String>()
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
type Fallible<T> = Result<T, crate::Error>;
#[test]
fn try_new_succeeds_with_correct_digest() -> Fallible<()> {
for correct_digest in
&["sha256:0000000000000000000000000000000000000000000000000000000000000000"]
{
ContentDigest::try_new(correct_digest.to_string())?;
}
Ok(())
}
#[test]
fn try_new_succeeds_with_incorrect_digest() {
for incorrect_digest in &[
"invalid",
"invalid:",
"invalid:0000000000000000000000000000000000000000000000000000000000000000",
] {
if ContentDigest::try_new(incorrect_digest.to_string()).is_ok() {
panic!(
"expected try_new to fail for incorrect digest {}",
incorrect_digest
);
}
}
}
#[test]
fn try_verify_succeeds_with_same_content() -> Fallible<()> {
let blob: &[u8] = b"somecontent";
let digest = DigestAlgorithm::Sha256.hash(&blob);
ContentDigest::try_new(digest)?
.try_verify(&blob)
.map_err(Into::into)
}
#[test]
fn try_verify_fails_with_different_content() -> Fallible<()> {
let blob: &[u8] = b"somecontent";
let different_blob: &[u8] = b"someothercontent";
let digest = DigestAlgorithm::Sha256.hash(&blob);
if ContentDigest::try_new(digest)?
.try_verify(&different_blob)
.is_ok()
{
panic!("expected try_verify to fail for a different blob");
}
Ok(())
}
}