use serde::{Deserialize, Serialize};
use std::fmt;
use crate::{Error, Result};
const SHA256_PREFIX: &str = "sha256:";
const SHA256_HEX_LEN: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct Digest(pub(super) String);
impl From<Digest> for String {
fn from(value: Digest) -> Self {
value.0
}
}
impl TryFrom<String> for Digest {
type Error = Error;
fn try_from(value: String) -> Result<Self> {
Self::parse(&value)
}
}
impl fmt::Display for Digest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Digest {
pub fn parse(digest: &str) -> Result<Self> {
Self::parse_within(digest, digest)
}
pub(super) fn parse_within(image: &str, digest: &str) -> Result<Self> {
if !is_digest(digest) {
return Err(Error::invalid_image(
image,
"the digest must be sha256: followed by 64 hex digits",
));
}
Ok(Self(digest.to_string()))
}
pub fn from_hex(hex: &str) -> Result<Self> {
Self::parse(&format!("{SHA256_PREFIX}{hex}"))
}
}
fn is_digest(digest: &str) -> bool {
digest.strip_prefix(SHA256_PREFIX).is_some_and(|hex| {
hex.len() == SHA256_HEX_LEN && hex.bytes().all(|byte| byte.is_ascii_hexdigit())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_digests() {
let digest = format!("sha256:{}", "a".repeat(64));
let parsed = Digest::parse(&digest).unwrap();
assert_eq!(parsed.to_string(), digest);
}
#[test]
fn refuses_malformed_digests() {
for digest in [
String::new(),
"sha256:".to_string(),
"sha256:short".to_string(),
format!("md5:{}", "a".repeat(64)),
format!("sha256:{}", "a".repeat(63)),
format!("sha256:{}", "a".repeat(65)),
format!("sha256:{}", "g".repeat(64)),
format!("sha256:{}@sha256:{}", "a".repeat(64), "b".repeat(64)),
] {
assert_eq!(
Digest::parse(&digest).unwrap_err().to_string(),
format!(
"invalid image \"{digest}\": the digest must be sha256: followed by 64 hex digits"
),
"{digest:?}"
);
}
}
#[test]
fn serde_runs_the_same_checks() {
let error = serde_json::from_str::<Digest>(r#""sha256:short""#).unwrap_err();
assert!(
error
.to_string()
.contains("the digest must be sha256: followed by 64 hex digits"),
"{error}"
);
}
}