use regex::Regex;
use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
pub const SEPARATOR: &str = ":";
static BLOB_URL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(.*)://(.*)/v2/(.*)/blobs/([^?]+)(?:\?.*)?$").unwrap());
pub fn is_blob_url(url: &str) -> bool {
BLOB_URL_REGEX.is_match(url)
}
static MANIFEST_URL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(.*)://(.*)/v2/(.*)/manifests/([^?]+)(?:\?.*)?$").unwrap());
pub fn is_manifest_digest_url(url: &str) -> bool {
Digest::extract_from_manifest_url(url).is_some()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Algorithm {
Crc32,
Sha256,
Sha512,
}
impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Algorithm::Crc32 => write!(f, "crc32"),
Algorithm::Sha256 => write!(f, "sha256"),
Algorithm::Sha512 => write!(f, "sha512"),
}
}
}
impl FromStr for Algorithm {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"crc32" => Ok(Algorithm::Crc32),
"sha256" => Ok(Algorithm::Sha256),
"sha512" => Ok(Algorithm::Sha512),
_ => Err(format!("invalid digest algorithm: {s}")),
}
}
}
pub struct Digest {
algorithm: Algorithm,
encoded: String,
}
impl Digest {
pub fn new(algorithm: Algorithm, encoded: String) -> Self {
Self { algorithm, encoded }
}
pub fn extract_from_blob_url(url: &str) -> Option<Self> {
BLOB_URL_REGEX
.captures(url)
.and_then(|caps| caps.get(4))
.map(|m| m.as_str())?
.parse()
.ok()
}
pub fn extract_from_manifest_url(url: &str) -> Option<Self> {
MANIFEST_URL_REGEX
.captures(url)
.and_then(|caps| caps.get(4))
.map(|m| m.as_str())?
.parse()
.ok()
}
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
pub fn encoded(&self) -> &str {
&self.encoded
}
}
impl fmt::Display for Digest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}{}", self.algorithm, SEPARATOR, self.encoded)
}
}
impl FromStr for Digest {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (algorithm, encoded) = s
.split_once(SEPARATOR)
.ok_or_else(|| format!("invalid digest: {s}"))?;
let algorithm = match algorithm {
"crc32" => {
if encoded.is_empty() {
return Err(format!("invalid crc32 digest: {s}"));
}
Algorithm::Crc32
}
"sha256" => {
if encoded.len() != 64 {
return Err(format!(
"invalid sha256 digest length: {}, expected 64",
encoded.len()
));
}
Algorithm::Sha256
}
"sha512" => {
if encoded.len() != 128 {
return Err(format!(
"invalid sha512 digest length: {}, expected 128",
encoded.len()
));
}
Algorithm::Sha512
}
_ => return Err(format!("invalid digest algorithm: {algorithm}")),
};
Ok(Digest::new(algorithm, encoded.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_from_blob_url() {
let test_cases = vec![
(
"http://registry.example.com/v2/library/ubuntu/blobs/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e",
Some((Algorithm::Sha256, "b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e")),
),
(
"https://registry.example.com/v2/myorg/myrepo/blobs/sha512:94381a28e8c039fedfa78de025158a068226c3ccd041b22c2c8e73fc993584e9b167d9ae32bc8b372c66701c808ab134e0768c8f16b9a3e61eec1ccf8faa9db8",
Some((Algorithm::Sha512, "94381a28e8c039fedfa78de025158a068226c3ccd041b22c2c8e73fc993584e9b167d9ae32bc8b372c66701c808ab134e0768c8f16b9a3e61eec1ccf8faa9db8")),
),
(
"https://registry.io/v2/org/team/project/blobs/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e",
Some((Algorithm::Sha256, "b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e")),
),
(
"http://localhost:5000/v2/myrepo/blobs/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e?ns=docker.io",
Some((Algorithm::Sha256, "b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e")),
),
("http://registry.example.com/blobs/sha256:abc", None),
("http://registry.example.com/v2/repo/manifests/sha256:abc", None),
("registry.example.com/v2/repo/blobs/sha256:abc", None),
("http://registry.example.com/v2/blobs/sha256:abc", None),
("", None),
("not-a-url", None),
("http://registry.example.com/v2/repo/blobs/invalid-digest", None),
("http://registry.example.com/v2/repo/blobs/md5:8a04994a666b4e4b20a2fd9e5a44f44c", None),
];
for (url, expected) in test_cases {
match expected {
Some((algorithm, encoded)) => {
let digest = Digest::extract_from_blob_url(url).unwrap();
assert_eq!(digest.algorithm(), algorithm);
assert_eq!(digest.encoded(), encoded);
}
None => assert!(Digest::extract_from_blob_url(url).is_none()),
}
}
}
#[test]
fn test_is_manifest_digest_url() {
let test_cases = vec![
(
"http://registry.example.com/v2/library/ubuntu/manifests/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e",
true,
),
(
"http://localhost:5000/v2/myrepo/manifests/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e?ns=docker.io",
true,
),
(
"http://registry.example.com/v2/library/ubuntu/manifests/latest",
false,
),
(
"http://registry.example.com/v2/library/ubuntu/manifests/md5:8a04994a666b4e4b20a2fd9e5a44f44c",
false,
),
(
"http://registry.example.com/v2/library/ubuntu/manifests/sha256:abc",
false,
),
(
"http://registry.example.com/v2/library/ubuntu/blobs/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e",
false,
),
("https://example.com/file.txt", false),
];
for (url, expected) in test_cases {
assert_eq!(is_manifest_digest_url(url), expected);
}
}
#[test]
fn test_extract_from_manifest_url() {
let test_cases = vec![
(
"http://registry.example.com/v2/library/ubuntu/manifests/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e",
Some((Algorithm::Sha256, "b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e")),
),
(
"http://localhost:5000/v2/myrepo/manifests/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e?ns=docker.io",
Some((Algorithm::Sha256, "b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e")),
),
(
"http://registry.example.com/v2/library/ubuntu/manifests/latest",
None,
),
(
"http://registry.example.com/v2/library/ubuntu/manifests/sha256:abc",
None,
),
(
"http://registry.example.com/v2/library/ubuntu/manifests/md5:8a04994a666b4e4b20a2fd9e5a44f44c",
None,
),
(
"http://registry.example.com/v2/library/ubuntu/blobs/sha256:b2c366cce7e68013d5441c6326d5a3e1b12aeb5ed58564d0fd3fa089bc29cb6e",
None,
),
("https://example.com/file.txt", None),
];
for (url, expected) in test_cases {
match expected {
Some((algorithm, encoded)) => {
let digest = Digest::extract_from_manifest_url(url).unwrap();
assert_eq!(digest.algorithm(), algorithm);
assert_eq!(digest.encoded(), encoded);
}
None => assert!(Digest::extract_from_manifest_url(url).is_none()),
}
}
}
#[test]
fn test_algorithm_display() {
let test_cases = vec![
(Algorithm::Crc32, "crc32"),
(Algorithm::Sha256, "sha256"),
(Algorithm::Sha512, "sha512"),
];
for (algorithm, expected) in test_cases {
assert_eq!(algorithm.to_string(), expected);
}
}
#[test]
fn test_algorithm_from_str() {
let test_cases = vec![
("crc32", Some(Algorithm::Crc32)),
("sha256", Some(Algorithm::Sha256)),
("sha512", Some(Algorithm::Sha512)),
("invalid", None),
];
for (s, expected) in test_cases {
assert_eq!(s.parse::<Algorithm>().ok(), expected);
}
}
#[test]
fn test_digest_display() {
let test_cases = vec![
(Algorithm::Crc32, "1475635037", "crc32:1475635037"),
(Algorithm::Sha256, "encoded_hash", "sha256:encoded_hash"),
(Algorithm::Sha512, "encoded_hash", "sha512:encoded_hash"),
];
for (algorithm, encoded, expected) in test_cases {
assert_eq!(
Digest::new(algorithm, encoded.to_string()).to_string(),
expected
);
}
}
}