use std::fmt::Display;
use std::str::FromStr;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum DigestAlgorithm {
Sha256,
Sha384,
Sha512,
Other(Box<str>),
}
impl AsRef<str> for DigestAlgorithm {
fn as_ref(&self) -> &str {
match self {
DigestAlgorithm::Sha256 => "sha256",
DigestAlgorithm::Sha384 => "sha384",
DigestAlgorithm::Sha512 => "sha512",
DigestAlgorithm::Other(o) => o,
}
}
}
impl Display for DigestAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_ref())
}
}
impl DigestAlgorithm {
pub const fn digest_hexlen(&self) -> Option<u32> {
match self {
DigestAlgorithm::Sha256 => Some(64),
DigestAlgorithm::Sha384 => Some(96),
DigestAlgorithm::Sha512 => Some(128),
DigestAlgorithm::Other(_) => None,
}
}
}
impl From<&str> for DigestAlgorithm {
fn from(value: &str) -> Self {
match value {
"sha256" => Self::Sha256,
"sha384" => Self::Sha384,
"sha512" => Self::Sha512,
o => Self::Other(o.into()),
}
}
}
fn char_is_lowercase_ascii_hex(c: char) -> bool {
matches!(c, '0'..='9' | 'a'..='f')
}
fn char_is_algorithm_component(c: char) -> bool {
matches!(c, 'a'..='z' | '0'..='9')
}
fn char_is_encoded(c: char) -> bool {
char_is_algorithm_component(c) || matches!(c, 'A'..='Z' | '=' | '_' | '-')
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Digest {
algorithm: DigestAlgorithm,
value: Box<str>,
split: usize,
}
impl AsRef<str> for Digest {
fn as_ref(&self) -> &str {
&self.value
}
}
impl Display for Digest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_ref())
}
}
impl<'de> serde::Deserialize<'de> for Digest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl serde::ser::Serialize for Digest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let v = self.to_string();
serializer.serialize_str(&v)
}
}
impl Digest {
const ALGORITHM_SEPARATOR: &'static [char] = &['+', '.', '_', '-'];
pub fn algorithm(&self) -> &DigestAlgorithm {
&self.algorithm
}
pub fn digest(&self) -> &str {
&self.value[self.split + 1..]
}
}
impl FromStr for Digest {
type Err = crate::OciSpecError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Digest::try_from(s)
}
}
impl TryFrom<String> for Digest {
type Error = crate::OciSpecError;
fn try_from(s: String) -> Result<Self, Self::Error> {
let s = s.into_boxed_str();
let Some(split) = s.find(':') else {
return Err(crate::OciSpecError::Other("missing ':' in digest".into()));
};
let (algorithm, value) = s.split_at(split);
let value = &value[1..];
let algorithm_parts = algorithm.split(Self::ALGORITHM_SEPARATOR);
for part in algorithm_parts {
if part.is_empty() {
return Err(crate::OciSpecError::Other(
"Empty algorithm component".into(),
));
}
if !part.chars().all(char_is_algorithm_component) {
return Err(crate::OciSpecError::Other(format!(
"Invalid algorithm component: {part}"
)));
}
}
if value.is_empty() {
return Err(crate::OciSpecError::Other("Empty algorithm value".into()));
}
if !value.chars().all(char_is_encoded) {
return Err(crate::OciSpecError::Other(format!(
"Invalid encoded value {value}"
)));
}
let algorithm = DigestAlgorithm::from(algorithm);
if let Some(expected) = algorithm.digest_hexlen() {
let found = value.len();
if expected as usize != found {
return Err(crate::OciSpecError::Other(format!(
"Invalid digest length {found} expected {expected}"
)));
}
let is_all_hex = value.chars().all(char_is_lowercase_ascii_hex);
if !is_all_hex {
return Err(crate::OciSpecError::Other(format!(
"Invalid non-hexadecimal character in digest: {value}"
)));
}
}
Ok(Self {
algorithm,
value: s,
split,
})
}
}
impl TryFrom<&str> for Digest {
type Error = crate::OciSpecError;
fn try_from(string: &str) -> Result<Self, Self::Error> {
TryFrom::try_from(string.to_owned())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Sha256Digest {
digest: Box<str>,
}
impl From<Sha256Digest> for Digest {
fn from(value: Sha256Digest) -> Self {
Self {
algorithm: DigestAlgorithm::Sha256,
value: format!("sha256:{}", value.digest()).into(),
split: 6,
}
}
}
impl AsRef<str> for Sha256Digest {
fn as_ref(&self) -> &str {
self.digest()
}
}
impl Display for Sha256Digest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.digest())
}
}
impl FromStr for Sha256Digest {
type Err = crate::OciSpecError;
fn from_str(digest: &str) -> Result<Self, Self::Err> {
let alg = DigestAlgorithm::Sha256;
let v = format!("{alg}:{digest}");
let d = Digest::from_str(&v)?;
match d.algorithm {
DigestAlgorithm::Sha256 => Ok(Self {
digest: d.digest().into(),
}),
o => Err(crate::OciSpecError::Other(format!(
"Expected algorithm sha256 but found {o}",
))),
}
}
}
impl Sha256Digest {
pub fn digest(&self) -> &str {
&self.digest
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_digest_invalid() {
let invalid = [
"",
"foo",
":",
"blah+",
"_digest:somevalue",
":blah",
"blah:",
"FooBar:123abc",
"^:foo",
"bar^baz:blah",
"sha256:123456*78",
"sha256:6c3c624b58dbbcd3c0dd82b4z53f04194d1247c6eebdaab7c610cf7d66709b3b", "sha384:x",
"sha384:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b",
"sha512:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b",
];
for case in invalid {
assert!(
Digest::from_str(case).is_err(),
"Should have failed to parse: {case}"
)
}
}
const VALID_DIGEST_SHA256: &str =
"sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b";
const VALID_DIGEST_SHA384: &str =
"sha384:6c3c624b58dbbcd4d1247c6eebdaab7c610cf7d66709b3b3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b";
const VALID_DIGEST_SHA512: &str =
"sha512:6c3c624b58dbbcd3c0dd826c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3bb4c53f04194d1247c6eebdaab7c610cf7d66709b3b";
#[test]
fn test_digest_valid() {
let cases = ["foo:bar", "xxhash:42"];
for case in cases {
Digest::from_str(case).unwrap();
}
let d = Digest::try_from("multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8")
.unwrap();
assert_eq!(d.algorithm(), &DigestAlgorithm::from("multihash+base58"));
assert_eq!(d.digest(), "QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8");
}
#[test]
fn test_sha256_valid() {
let expected_value = VALID_DIGEST_SHA256.split_once(':').unwrap().1;
let d = Digest::from_str(VALID_DIGEST_SHA256).unwrap();
assert_eq!(d.algorithm(), &DigestAlgorithm::Sha256);
assert_eq!(d.digest(), expected_value);
let base_digest = Digest::from(d.clone());
assert_eq!(base_digest.digest(), expected_value);
}
#[test]
fn test_sha384_valid() {
let expected_value = VALID_DIGEST_SHA384.split_once(':').unwrap().1;
let d = Digest::from_str(VALID_DIGEST_SHA384).unwrap();
assert_eq!(d.algorithm(), &DigestAlgorithm::Sha384);
assert_eq!(d.digest(), expected_value);
assert_eq!(d.as_ref(), VALID_DIGEST_SHA384);
let base_digest = Digest::from(d.clone());
assert_eq!(base_digest.digest(), expected_value);
}
#[test]
fn test_sha512_valid() {
let expected_value = VALID_DIGEST_SHA512.split_once(':').unwrap().1;
let d = Digest::from_str(VALID_DIGEST_SHA512).unwrap();
assert_eq!(d.algorithm(), &DigestAlgorithm::Sha512);
assert_eq!(d.digest(), expected_value);
let base_digest = Digest::from(d.clone());
assert_eq!(base_digest.digest(), expected_value);
}
#[test]
fn test_sha256() {
let digest = VALID_DIGEST_SHA256.split_once(':').unwrap().1;
let v = Sha256Digest::from_str(digest).unwrap();
assert_eq!(v.digest(), digest);
}
}