use std::{
borrow::Borrow,
fmt::{self, Display, Formatter},
};
use thiserror::Error;
use super::char_is_alnum;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Digest<'a>(&'a str);
impl<'a> Digest<'a> {
pub fn new(digest: &'a str) -> Result<Self, InvalidDigestError> {
let (algorithm, encoded) = digest
.split_once(':')
.ok_or(InvalidDigestError::MissingAlgorithm)?;
let mut separators: u8 = 0;
for char in algorithm.chars() {
match char {
'a'..='z' | '0'..='9' => {
separators = 0;
Ok(())
}
'+' | '.' | '_' | '-' => {
separators += 1;
if separators <= 1 {
Ok(())
} else {
Err(InvalidDigestError::AlgorithmSeparators)
}
}
char => Err(InvalidDigestError::AlgorithmCharacter(char)),
}?;
}
for char in encoded.chars() {
if !matches!(char, 'a'..='z' | 'A'..='Z' | '0'..='9' | '=' | '_' | '-') {
return Err(InvalidDigestError::EncodeCharacter(char));
}
}
if algorithm.is_empty() {
Err(InvalidDigestError::MissingAlgorithm)
} else if !algorithm.starts_with(char_is_alnum) {
Err(InvalidDigestError::AlgorithmStart)
} else if !algorithm.ends_with(char_is_alnum) {
Err(InvalidDigestError::AlgorithmEnd)
} else if encoded.is_empty() {
Err(InvalidDigestError::EncodeEmpty)
} else {
Ok(Self(digest))
}
}
pub(super) const fn new_unchecked(digest: &'a str) -> Self {
Self(digest)
}
#[must_use]
pub const fn into_inner(self) -> &'a str {
self.0
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidDigestError {
#[error("image digest is missing its algorithm")]
MissingAlgorithm,
#[error("image digest algorithms may only have one separator (+._-) in a row")]
AlgorithmSeparators,
#[error(
"invalid character '{0}' in image digest algorithm,
digests may only contain lowercase ASCII letters (a-z), digits (0-9), or separators (+._-)"
)]
AlgorithmCharacter(char),
#[error(
"invalid character '{0}' in image digest encode, data encoded in digest may only contain \
ASCII letters (a-z, A-Z), digits (0-9), equals (=), underscores (_), and dashes (-)"
)]
EncodeCharacter(char),
#[error(
"image digest algorithm must start with a lowercase ASCII letter (a-z) or a digit (0-9)"
)]
AlgorithmStart,
#[error(
"image digest algorithm must end with a lowercase ASCII letter (a-z) or a digit (0-9)"
)]
AlgorithmEnd,
#[error("image digest encode is empty")]
EncodeEmpty,
}
impl<'a> AsRef<str> for Digest<'a> {
fn as_ref(&self) -> &str {
self.0
}
}
impl<'a> Borrow<str> for Digest<'a> {
fn borrow(&self) -> &str {
self.0
}
}
impl<'a> TryFrom<&'a str> for Digest<'a> {
type Error = InvalidDigestError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl<'a> PartialEq<str> for Digest<'a> {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl<'a> PartialEq<&str> for Digest<'a> {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl<'a> Display for Digest<'a> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(self.0)
}
}
#[cfg(test)]
mod tests {
use pomsky_macro::pomsky;
use proptest::proptest;
use super::*;
const DIGEST: &str = pomsky! {
let component = [ascii_lower ascii_digit]+;
let separator = ['+' '.' '_' '-'];
let algorithm = component (separator component)*;
let encoded = [ascii_alnum '=' '_' '-']+;
algorithm ":" encoded
};
proptest! {
#[test]
fn no_panic(digest: String) {
let _ = Digest::new(&digest);
}
#[test]
fn new(digest in DIGEST) {
Digest::new(&digest)?;
}
}
}