use core::fmt;
use sha2::{Digest as _, Sha256};
pub const SHA256_SIZE: usize = 32;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Digest([u8; SHA256_SIZE]);
impl Digest {
pub fn parse(value: &str) -> Result<Self, DigestError> {
let encoded = value
.strip_prefix("sha256:")
.ok_or(DigestError::UnsupportedAlgorithm)?;
if encoded.len() != SHA256_SIZE * 2 {
return Err(DigestError::InvalidLength);
}
let mut bytes = [0; SHA256_SIZE];
for (index, pair) in encoded.as_bytes().chunks_exact(2).enumerate() {
bytes[index] = (hex(pair[0])? << 4) | hex(pair[1])?;
}
Ok(Self(bytes))
}
pub const fn from_bytes(bytes: [u8; SHA256_SIZE]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; SHA256_SIZE] {
&self.0
}
}
impl fmt::Display for Digest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("sha256:")?;
for byte in self.0 {
write!(formatter, "{byte:02x}")?;
}
Ok(())
}
}
pub struct Verifier {
expected_digest: Digest,
expected_size: Option<u64>,
actual_size: u64,
hash: Sha256,
}
impl Verifier {
pub fn new(expected_digest: Digest, expected_size: u64) -> Self {
Self {
expected_digest,
expected_size: Some(expected_size),
actual_size: 0,
hash: Sha256::new(),
}
}
pub fn digest_only(expected_digest: Digest) -> Self {
Self {
expected_digest,
expected_size: None,
actual_size: 0,
hash: Sha256::new(),
}
}
pub fn update(&mut self, bytes: &[u8]) -> Result<(), VerifyError> {
self.actual_size = self
.actual_size
.checked_add(bytes.len() as u64)
.ok_or(VerifyError::SizeOverflow)?;
self.hash.update(bytes);
Ok(())
}
pub const fn actual_size(&self) -> u64 {
self.actual_size
}
pub fn finish(&self) -> Result<(), VerifyError> {
if let Some(expected_size) = self.expected_size.filter(|size| *size != self.actual_size) {
return Err(VerifyError::SizeMismatch {
expected: expected_size,
actual: self.actual_size,
});
}
let actual = Digest::from_bytes(self.hash.clone().finalize().into());
if actual != self.expected_digest {
return Err(VerifyError::DigestMismatch {
expected: self.expected_digest,
actual,
});
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
pub enum DigestError {
#[display("unsupported digest algorithm")]
UnsupportedAlgorithm,
#[display("invalid SHA-256 digest length")]
InvalidLength,
#[display("invalid SHA-256 digest encoding")]
InvalidEncoding,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
pub enum VerifyError {
#[display("content size overflow")]
SizeOverflow,
#[display("size mismatch: expected {expected}, got {actual}")]
SizeMismatch { expected: u64, actual: u64 },
#[display("digest mismatch: expected {expected}, got {actual}")]
DigestMismatch { expected: Digest, actual: Digest },
}
fn hex(byte: u8) -> Result<u8, DigestError> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
_ => Err(DigestError::InvalidEncoding),
}
}
#[cfg(test)]
mod tests {
use std::{format, string::ToString};
use super::{Digest, DigestError, Verifier, VerifyError};
#[test]
fn parses_and_formats_sha256() {
let text = "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
assert_eq!(Digest::parse(text).unwrap().to_string(), text);
}
#[test]
fn reports_digest_parse_errors() {
assert_eq!(
Digest::parse(
"sha512:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
),
Err(DigestError::UnsupportedAlgorithm)
);
assert_eq!(Digest::parse("sha256:00"), Err(DigestError::InvalidLength));
assert_eq!(
Digest::parse(
"sha256:ga7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
),
Err(DigestError::InvalidEncoding)
);
assert_eq!(
DigestError::UnsupportedAlgorithm.to_string(),
"unsupported digest algorithm"
);
assert_eq!(
DigestError::InvalidLength.to_string(),
"invalid SHA-256 digest length"
);
assert_eq!(
DigestError::InvalidEncoding.to_string(),
"invalid SHA-256 digest encoding"
);
}
#[test]
fn verifies_fragmented_content() {
let digest = Digest::parse(
"sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
)
.unwrap();
let mut verifier = Verifier::new(digest, 3);
assert_eq!(verifier.actual_size(), 0);
verifier.update(b"a").unwrap();
assert_eq!(verifier.actual_size(), 1);
verifier.update(b"bc").unwrap();
assert_eq!(verifier.actual_size(), 3);
assert_eq!(verifier.finish(), Ok(()));
let mut verifier = Verifier::new(digest, 4);
verifier.update(b"abc").unwrap();
assert!(matches!(
verifier.finish(),
Err(VerifyError::SizeMismatch { .. })
));
}
#[test]
fn formats_verification_errors() {
let expected = Digest::from_bytes([0x11; 32]);
let actual = Digest::from_bytes([0x22; 32]);
assert_eq!(
VerifyError::SizeOverflow.to_string(),
"content size overflow"
);
assert_eq!(
VerifyError::SizeMismatch {
expected: 7,
actual: 3,
}
.to_string(),
"size mismatch: expected 7, got 3"
);
assert_eq!(
VerifyError::DigestMismatch { expected, actual }.to_string(),
format!("digest mismatch: expected {expected}, got {actual}")
);
}
}