#[cfg(test)] mod tests;
use crate::{
errors::ImageError,
filesystem::{storage::FileStorage, vfs::Filesystem},
manifest::RuntimeConfig,
};
use regex::Regex;
use sha2::{Digest, Sha256};
use std::{
cmp::{Ord, Ordering, PartialOrd},
fmt,
hash::{Hash, Hasher},
io::Write,
ops::Range,
str,
str::FromStr,
};
pub struct Image {
pub(crate) name: ImageName,
pub(crate) config: RuntimeConfig,
pub(crate) filesystem: Filesystem,
pub(crate) storage: FileStorage,
}
impl Image {
pub fn content_digest(&self) -> ContentDigest {
self.name()
.content_digest()
.expect("loaded images must always have a digest")
}
pub fn name(&self) -> &ImageName {
&self.name
}
}
impl fmt::Debug for Image {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Image({})", self.name)
}
}
#[derive(Clone)]
pub struct ImageName {
serialized: String,
registry_pos: Option<Range<usize>>,
repository_pos: Range<usize>,
tag_pos: Option<Range<usize>>,
digest_pos: Option<Range<usize>>,
}
impl ImageName {
pub fn as_str(&self) -> &str {
&self.serialized
}
pub fn from_parts(
registry: Option<&str>,
repository: &str,
tag: Option<&str>,
digest: Option<&str>,
) -> Result<Self, ImageError> {
let mut buffer = Vec::new();
if let Some(registry) = registry {
write!(&mut buffer, "{}/", registry)?;
}
write!(&mut buffer, "{}", repository)?;
if let Some(tag) = tag {
write!(&mut buffer, ":{}", tag)?;
}
if let Some(digest) = digest {
write!(&mut buffer, "@{}", digest)?;
}
let combined = str::from_utf8(&buffer).unwrap();
let parsed = ImageName::parse(combined)?;
if parsed.registry_str().as_deref() == registry
&& parsed.repository_str() == repository
&& parsed.tag_str().as_deref() == tag
&& parsed.content_digest_str().as_deref() == digest
{
Ok(parsed)
} else {
Err(ImageError::InvalidReferenceFormat(combined.to_owned()))
}
}
pub fn as_parts(&self) -> (Option<&str>, &str, Option<&str>, Option<&str>) {
(
self.registry_str(),
self.repository_str(),
self.tag_str(),
self.content_digest_str(),
)
}
pub fn version(&self) -> ImageVersion {
if self.content_digest_str().is_some() {
return ImageVersion::ContentDigest(self.content_digest().unwrap());
}
if self.tag_str().is_some() {
return ImageVersion::Tag(self.tag().unwrap());
}
ImageVersion::Tag(Tag::latest())
}
pub fn parse(s: &str) -> Result<Self, ImageError> {
lazy_static! {
static ref HAS_REGISTRY: Regex = Regex::new(concat!(
"^",
"(?:",
"(?:",
"(?:",
"[a-zA-Z0-9]|",
"[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]",
")",
"(?:",
"\\.",
"(?:",
"[a-zA-Z0-9]|",
"[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]",
")",
")+",
"(?::[0-9]+)?",
")",
"|(?:",
"(?:",
"[a-zA-Z0-9]|",
"[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]",
")",
"(?::[0-9]+)",
")",
"|(?:",
"localhost",
"(?::[0-9]+)?",
")",
")",
"/",
)).unwrap();
static ref WITH_REGISTRY: Regex = Regex::new(&format!(
"^{}/{}(:{})?(@{})?$",
Registry::regex_str(),
Repository::regex_str(),
Tag::regex_str(),
ContentDigest::regex_str()
))
.unwrap();
static ref NO_REGISTRY: Regex = Regex::new(&format!(
"^{}(:{})?(@{})?$",
Repository::regex_str(),
Tag::regex_str(),
ContentDigest::regex_str()
))
.unwrap();
}
if HAS_REGISTRY.is_match(s) {
match WITH_REGISTRY.captures(s) {
None => Err(ImageError::InvalidReferenceFormat(s.to_owned())),
Some(captures) => Ok(ImageName {
serialized: s.to_owned(),
registry_pos: Some(captures.name("reg").unwrap().range()),
repository_pos: captures.name("repo").unwrap().range(),
tag_pos: captures.name("tag").map(|m| m.range()),
digest_pos: captures.name("dig").map(|m| m.range()),
}),
}
} else {
match NO_REGISTRY.captures(s) {
None => Err(ImageError::InvalidReferenceFormat(s.to_owned())),
Some(captures) => Ok(ImageName {
serialized: s.to_owned(),
registry_pos: None,
repository_pos: captures.name("repo").unwrap().range(),
tag_pos: captures.name("tag").map(|m| m.range()),
digest_pos: captures.name("dig").map(|m| m.range()),
}),
}
}
}
pub fn registry_str(&self) -> Option<&str> {
self.registry_pos
.as_ref()
.map(|pos| &self.serialized[pos.clone()])
}
pub fn repository_str(&self) -> &str {
&self.serialized[self.repository_pos.clone()]
}
pub fn tag_str(&self) -> Option<&str> {
self.tag_pos
.as_ref()
.map(|pos| &self.serialized[pos.clone()])
}
pub fn content_digest_str(&self) -> Option<&str> {
self.digest_pos
.as_ref()
.map(|pos| &self.serialized[pos.clone()])
}
pub fn registry(&self) -> Option<Registry> {
self.registry_str()
.map(|s| Registry::parse(s).expect("already parsed"))
}
pub fn repository(&self) -> Repository {
Repository::parse(self.repository_str()).expect("already parsed")
}
pub fn tag(&self) -> Option<Tag> {
self.tag_str()
.map(|s| Tag::parse(s).expect("already parsed"))
}
pub fn content_digest(&self) -> Option<ContentDigest> {
self.content_digest_str()
.map(|s| ContentDigest::parse(s).expect("already parsed"))
}
pub fn with_found_digest(&self, found_digest: &ContentDigest) -> Result<ImageName, ImageError> {
match self.content_digest() {
None => ImageName::from_parts(
self.registry_str(),
self.repository_str(),
self.tag_str(),
Some(found_digest.as_str()),
),
Some(image_digest) if &image_digest == found_digest => Ok(self.clone()),
Some(image_digest) => {
return Err(ImageError::ContentDigestMismatch {
expected: image_digest.clone(),
found: found_digest.clone(),
})
}
}
}
}
impl Eq for ImageName {}
impl PartialEq for ImageName {
fn eq(&self, other: &Self) -> bool {
self.serialized.eq(&other.serialized)
}
}
impl FromStr for ImageName {
type Err = ImageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ImageName::parse(s)
}
}
impl fmt::Display for ImageName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl fmt::Debug for ImageName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl Hash for ImageName {
fn hash<H: Hasher>(&self, state: &mut H) {
self.serialized.hash(state);
}
}
impl Ord for ImageName {
fn cmp(&self, other: &Self) -> Ordering {
self.serialized.cmp(&other.serialized)
}
}
impl PartialOrd for ImageName {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.serialized.partial_cmp(&other.serialized)
}
}
#[derive(Clone)]
pub struct Registry {
serialized: String,
domain_pos: Range<usize>,
port: Option<u16>,
is_https: bool,
}
impl Registry {
pub fn as_str(&self) -> &str {
&self.serialized
}
pub fn parse(s: &str) -> Result<Self, ImageError> {
lazy_static! {
static ref RE: Regex = Regex::new(&format!("^{}$", Registry::regex_str(),)).unwrap();
}
match RE.captures(s) {
None => Err(ImageError::InvalidReferenceFormat(s.to_owned())),
Some(captures) => {
let domain = captures.name("reg_d").unwrap();
Ok(Registry {
serialized: s.to_owned(),
domain_pos: domain.range(),
is_https: domain.as_str().contains('.'),
port: captures.name("reg_p").map(|m| m.as_str().parse().unwrap()),
})
}
}
}
pub fn domain_str(&self) -> &str {
&self.serialized[self.domain_pos.clone()]
}
pub fn port(&self) -> Option<u16> {
self.port
}
pub fn is_https(&self) -> bool {
self.is_https
}
pub fn protocol_str(&self) -> &str {
if self.is_https() {
"https"
} else {
"http"
}
}
fn regex_str() -> &'static str {
concat!(
"(?P<reg>",
"(?P<reg_d>",
"(?:",
"[a-zA-Z0-9]|",
"[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]",
")",
"(?:",
"\\.",
"(?:",
"[a-zA-Z0-9]|",
"[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]",
")",
")*",
")",
"(?:",
"[:]",
"(?P<reg_p>",
"[0-9]+",
")",
")?",
")",
)
}
}
impl Eq for Registry {}
impl PartialEq for Registry {
fn eq(&self, other: &Self) -> bool {
self.serialized.eq(&other.serialized)
}
}
impl FromStr for Registry {
type Err = ImageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Registry::parse(s)
}
}
impl fmt::Display for Registry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl fmt::Debug for Registry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl Hash for Registry {
fn hash<H: Hasher>(&self, state: &mut H) {
self.serialized.hash(state);
}
}
impl Ord for Registry {
fn cmp(&self, other: &Self) -> Ordering {
self.serialized.cmp(&other.serialized)
}
}
impl PartialOrd for Registry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.serialized.partial_cmp(&other.serialized)
}
}
#[derive(Clone)]
pub struct Repository {
serialized: String,
}
pub struct RepositoryIter<'a> {
remaining: Option<&'a str>,
}
impl<'a> Iterator for RepositoryIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
self.remaining.map(|remaining| {
let mut parts = remaining.splitn(2, '/');
let first = parts.next().unwrap();
let second = parts.next();
self.remaining = second;
first
})
}
}
impl Repository {
pub fn as_str(&self) -> &str {
&self.serialized
}
pub fn parse(s: &str) -> Result<Self, ImageError> {
lazy_static! {
static ref RE: Regex = Regex::new(&format!("^{}$", Repository::regex_str(),)).unwrap();
}
match RE.is_match(s) {
false => Err(ImageError::InvalidReferenceFormat(s.to_owned())),
true => Ok(Repository {
serialized: s.to_owned(),
}),
}
}
pub fn iter(&self) -> RepositoryIter {
RepositoryIter {
remaining: Some(&self.serialized),
}
}
pub fn join(&self, other: &Self) -> Self {
Repository {
serialized: format!("{}/{}", self.serialized, other.serialized),
}
}
fn regex_str() -> &'static str {
concat!(
"(?P<repo>",
"(?:",
"[a-z0-9]+",
"(?:",
"(?:[._]|__|[-]*)",
"[a-z0-9]+",
")*",
")",
"(?:",
"/",
"[a-z0-9]+",
"(?:",
"(?:[._]|__|[-]*)",
"[a-z0-9]+",
")*",
")*",
")"
)
}
}
impl Eq for Repository {}
impl PartialEq for Repository {
fn eq(&self, other: &Self) -> bool {
self.serialized.eq(&other.serialized)
}
}
impl FromStr for Repository {
type Err = ImageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Repository::parse(s)
}
}
impl fmt::Display for Repository {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl fmt::Debug for Repository {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl Hash for Repository {
fn hash<H: Hasher>(&self, state: &mut H) {
self.serialized.hash(state);
}
}
impl Ord for Repository {
fn cmp(&self, other: &Self) -> Ordering {
self.serialized.cmp(&other.serialized)
}
}
impl PartialOrd for Repository {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.serialized.partial_cmp(&other.serialized)
}
}
#[derive(Clone)]
pub struct Tag {
serialized: String,
}
static LATEST_STR: &str = "latest";
impl Tag {
pub fn as_str(&self) -> &str {
&self.serialized
}
pub fn parse(s: &str) -> Result<Self, ImageError> {
lazy_static! {
static ref RE: Regex = Regex::new(&format!("^{}$", Tag::regex_str(),)).unwrap();
}
match RE.is_match(s) {
false => Err(ImageError::InvalidReferenceFormat(s.to_owned())),
true => Ok(Tag {
serialized: s.to_owned(),
}),
}
}
pub fn latest() -> Self {
Tag {
serialized: LATEST_STR.to_owned(),
}
}
pub fn is_latest(&self) -> bool {
self.serialized == LATEST_STR
}
fn regex_str() -> &'static str {
"(?P<tag>[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127})"
}
}
impl Eq for Tag {}
impl PartialEq for Tag {
fn eq(&self, other: &Tag) -> bool {
self.serialized.eq(&other.serialized)
}
}
impl FromStr for Tag {
type Err = ImageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Tag::parse(s)
}
}
impl fmt::Display for Tag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl fmt::Debug for Tag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl Hash for Tag {
fn hash<H: Hasher>(&self, state: &mut H) {
self.serialized.hash(state);
}
}
impl Ord for Tag {
fn cmp(&self, other: &Self) -> Ordering {
self.serialized.cmp(&other.serialized)
}
}
impl PartialOrd for Tag {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.serialized.partial_cmp(&other.serialized)
}
}
#[derive(Clone)]
pub struct ContentDigest {
serialized: String,
format_pos: Range<usize>,
hex_pos: Range<usize>,
}
impl Eq for ContentDigest {}
impl PartialEq for ContentDigest {
fn eq(&self, other: &Self) -> bool {
self.serialized.eq(&other.serialized)
}
}
impl FromStr for ContentDigest {
type Err = ImageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ContentDigest::parse(s)
}
}
impl fmt::Display for ContentDigest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl fmt::Debug for ContentDigest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl Hash for ContentDigest {
fn hash<H: Hasher>(&self, state: &mut H) {
self.serialized.hash(state);
}
}
impl Ord for ContentDigest {
fn cmp(&self, other: &Self) -> Ordering {
self.serialized.cmp(&other.serialized)
}
}
impl PartialOrd for ContentDigest {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.serialized.partial_cmp(&other.serialized)
}
}
impl ContentDigest {
pub fn as_str(&self) -> &str {
&self.serialized
}
pub fn from_parts<T: fmt::LowerHex>(
format_part: &str,
hex_part: &T,
) -> Result<Self, ImageError> {
ContentDigest::parse(&format!("{}:{:x}", format_part, hex_part))
}
pub fn from_content(content_bytes: &[u8]) -> Self {
ContentDigest::from_parts("sha256", &Sha256::digest(content_bytes)).unwrap()
}
pub fn parse(s: &str) -> Result<Self, ImageError> {
lazy_static! {
static ref RE: Regex =
Regex::new(&format!("^{}$", ContentDigest::regex_str(),)).unwrap();
}
match RE.captures(s) {
None => Err(ImageError::InvalidReferenceFormat(s.to_owned())),
Some(captures) => Ok(ContentDigest {
serialized: s.to_owned(),
format_pos: captures.name("dig_f").unwrap().range(),
hex_pos: captures.name("dig_h").unwrap().range(),
}),
}
}
pub fn format_str(&self) -> &str {
&self.serialized[self.format_pos.clone()]
}
pub fn hex_str(&self) -> &str {
&self.serialized[self.hex_pos.clone()]
}
fn regex_str() -> &'static str {
concat!(
"(?P<dig>",
"(?P<dig_f>",
"(?:",
"[a-zA-Z]",
"[a-zA-Z0-9]*",
")",
"(?:",
"[-_+.]",
"[a-zA-Z]",
"[a-zA-Z0-9]*",
")*",
")",
"[:]",
"(?P<dig_h>",
"[a-f0-9]{32,}",
")",
")",
)
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ImageVersion {
Tag(Tag),
ContentDigest(ContentDigest),
}
impl ImageVersion {
pub fn as_str(&self) -> &str {
match self {
ImageVersion::Tag(tag) => tag.as_str(),
ImageVersion::ContentDigest(content_digest) => content_digest.as_str(),
}
}
pub fn parse(s: &str) -> Result<Self, ImageError> {
if s.contains(':') {
Ok(ImageVersion::ContentDigest(ContentDigest::parse(s)?))
} else {
Ok(ImageVersion::Tag(Tag::parse(s)?))
}
}
pub fn is_content_digest(&self) -> bool {
match self {
ImageVersion::Tag(_) => false,
ImageVersion::ContentDigest(_) => true,
}
}
pub fn is_tag(&self) -> bool {
match self {
ImageVersion::Tag(_) => true,
ImageVersion::ContentDigest(_) => false,
}
}
}
impl FromStr for ImageVersion {
type Err = ImageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ImageVersion::parse(s)
}
}
impl fmt::Display for ImageVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl fmt::Debug for ImageVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}