use serde::{Deserialize, Serialize};
use std::fmt;
use crate::{Error, Result};
mod archive;
mod digest;
mod reference;
mod resolve;
pub use archive::Archive;
pub use digest::Digest;
pub use reference::Reference;
const STDIN_IMAGE: &str = "-";
const LOCAL_PATH_PREFIXES: [&str; 3] = ["/", "./", "../"];
const ARCHIVE_SUFFIXES: [&str; 3] = [".tar", ".tar.gz", ".tgz"];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub enum Image {
Archive(Archive),
Registry(Reference),
}
impl From<Image> for String {
fn from(value: Image) -> Self {
value.as_str().to_string()
}
}
impl TryFrom<String> for Image {
type Error = Error;
fn try_from(value: String) -> Result<Self> {
Self::parse(&value)
}
}
impl fmt::Display for Image {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl Default for Image {
fn default() -> Self {
Self::Registry(Reference::default())
}
}
impl Image {
pub fn parse(image: &str) -> Result<Self> {
if Self::is_local(image) {
Archive::parse(image).map(Self::Archive)
} else {
Reference::parse(image).map(Self::Registry)
}
}
#[must_use]
pub fn is_local(image: &str) -> bool {
image == STDIN_IMAGE
|| LOCAL_PATH_PREFIXES
.iter()
.any(|prefix| image.starts_with(prefix))
|| ARCHIVE_SUFFIXES
.iter()
.any(|suffix| image.ends_with(suffix))
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Archive(archive) => archive.as_str(),
Self::Registry(reference) => reference.as_str(),
}
}
#[must_use]
pub fn pinned(&self, digest: &Digest) -> String {
match self {
Self::Archive(archive) => archive.as_str().to_string(),
Self::Registry(reference) => reference.pinned(digest),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_local_copies_the_engine_rule() {
let local_images = [
"/abs/x.tar",
"./x.tar",
"../x.tar",
"-",
"x.tar",
"x.tar.gz",
"x.tgz",
"/dir",
"/images/../dir",
"/images//./dir",
"./images/../dir",
"../images//./dir",
"images/../x.tar",
"images//x.tar.gz",
"images/./x.tgz",
"images/../nested//./x.tar.gz",
];
for image in local_images {
assert!(Image::is_local(image), "local image: {image:?}");
}
let references = [
"alpine",
"alpine:latest",
"ghcr.io/org/name:latest",
"localhost:5000/x",
"tarball",
"",
"x.zip",
"xtar",
"xtgz",
"xgz",
"x.gz",
"xtar.gz",
"x.gz.bak",
"x.tar.bak",
"x.tar.gz.bak",
"x.tgz.bak",
"images/../nested//./x.gz",
"images.tar/alpine:latest",
"alpine:tar",
"x.TAR",
"x.TAR.GZ",
"x.TGZ",
"x.GZ",
"images/../x.gZ",
"images/../x.TaR.gZ",
".",
"..",
];
for image in references {
assert!(!Image::is_local(image), "registry reference: {image:?}");
}
}
#[test]
fn parses_images() {
let digest = format!("sha256:{}", "a".repeat(64));
for (image, is_archive) in [
("/abs/guest.tar".to_string(), true),
("ghcr.io/org/name:latest".to_string(), false),
(format!("localhost:5000/org/name@{digest}"), false),
] {
let parsed = Image::parse(&image).unwrap();
if is_archive {
assert!(matches!(parsed, Image::Archive(_)), "{image}");
} else {
assert!(matches!(parsed, Image::Registry(_)), "{image}");
}
assert_eq!(parsed.as_str(), image);
assert_eq!(parsed.to_string(), image);
}
}
#[test]
fn refuses_malformed_images() {
let cases = [
(
"./guest.tar",
"a local image must be an absolute path to a docker save archive",
),
(
"alpine",
"a registry reference must start with its registry host",
),
];
for (image, reason) in cases {
assert_eq!(
Image::parse(image).unwrap_err().to_string(),
format!("invalid image \"{image}\": {reason}"),
"{image:?}"
);
}
}
#[test]
fn pins_an_image() {
let hex = "a".repeat(64);
let digest = Digest::parse(&format!("sha256:{hex}")).unwrap();
let registry = Image::parse("ghcr.io/org/name:latest").unwrap();
assert_eq!(
registry.pinned(&digest),
format!("ghcr.io/org/name@sha256:{hex}")
);
let archive = Image::parse("/abs/guest.tar").unwrap();
assert_eq!(archive.pinned(&digest), "/abs/guest.tar");
}
#[test]
fn serde_runs_the_same_checks() {
let error = serde_json::from_str::<Image>(r#""./guest.tar""#).unwrap_err();
assert!(
error
.to_string()
.contains("a local image must be an absolute path to a docker save archive"),
"{error}"
);
let image = Image::parse("/abs/guest.tar").unwrap();
let json = serde_json::to_string(&image).unwrap();
assert_eq!(json, r#""/abs/guest.tar""#);
assert_eq!(serde_json::from_str::<Image>(&json).unwrap(), image);
}
}