use anyhow::{bail, Result};
use oci_spec::image::Digest;
use regex::Regex;
use std::{fmt, str::FromStr};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Reference(String);
impl std::ops::Deref for Reference {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl fmt::Display for Reference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
lazy_static::lazy_static! {
static ref REF_RE: Regex = Regex::new(r"^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$").unwrap();
}
impl Reference {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn encoded(&self) -> String {
self.0
.chars()
.map(|c| {
if c.is_ascii_uppercase() || c == ':' {
format!("%{:02X}", c as u8)
} else {
c.to_string()
}
})
.collect()
}
pub fn new(name: &str) -> Result<Self> {
if REF_RE.is_match(name) {
Ok(Reference(name.to_string()))
} else if name.contains(':') {
_ = Digest::from_str(name)?;
Ok(Reference(name.to_string()))
} else {
bail!("Invalid reference {name}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reference() {
assert_eq!(Reference::new("latest").unwrap().as_str(), "latest");
assert_eq!(
Reference::new(
"sha256:a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
)
.unwrap()
.as_str(),
"sha256:a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
);
assert!(Reference::new("my_super_tag@2").is_err());
assert_eq!(
Reference::new("SuperTag").unwrap().encoded(),
"%53uper%54ag"
);
assert_eq!(
Reference::new(
"sha256:a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
)
.unwrap()
.encoded(),
"sha256%3Aa1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
);
}
}