pub use oci_client::Reference as ImageReference;
pub mod image_ref_serde {
use super::ImageReference;
use serde::{Deserialize, Deserializer, Serializer};
use std::str::FromStr;
pub fn serialize<S: Serializer>(r: &ImageReference, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&r.to_string())
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<ImageReference, D::Error> {
let s = String::deserialize(d)?;
ImageReference::from_str(&s).map_err(serde::de::Error::custom)
}
pub mod option {
use super::ImageReference;
use serde::{Deserialize, Deserializer, Serializer};
use std::str::FromStr;
pub fn serialize<S: Serializer>(
r: &Option<ImageReference>,
s: S,
) -> Result<S::Ok, S::Error> {
match r {
Some(r) => s.serialize_str(&r.to_string()),
None => s.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
d: D,
) -> Result<Option<ImageReference>, D::Error> {
let s: Option<String> = Option::deserialize(d)?;
match s {
Some(s) => ImageReference::from_str(&s)
.map(Some)
.map_err(serde::de::Error::custom),
None => Ok(None),
}
}
}
}
#[derive(Debug, Clone)]
pub struct ImageRef {
parsed: ImageReference,
original: String,
}
impl ImageRef {
#[must_use]
pub fn from_parsed(parsed: ImageReference) -> Self {
let original = parsed.to_string();
Self { parsed, original }
}
#[must_use]
pub fn parsed(&self) -> &ImageReference {
&self.parsed
}
#[must_use]
pub fn original(&self) -> &str {
&self.original
}
#[must_use]
pub fn is_unqualified(&self) -> bool {
image_str_is_unqualified(&self.original)
}
}
#[must_use]
pub fn image_str_is_unqualified(s: &str) -> bool {
let without_digest = match s.split_once('@') {
Some((head, _)) => head,
None => s,
};
let Some((head, _rest)) = without_digest.split_once('/') else {
return true;
};
if head == "localhost" {
return false;
}
if head.contains('.') || head.contains(':') {
return false;
}
true
}
#[must_use]
pub fn image_ref_candidates(image: &str) -> Vec<(String, String)> {
let (primary, reference) = if let Some(at_pos) = image.find('@') {
(image[..at_pos].to_string(), image[at_pos + 1..].to_string())
} else if let Some(colon_pos) = image.rfind(':') {
let potential_tag = &image[colon_pos + 1..];
if !potential_tag.contains('/') && !potential_tag.is_empty() {
(image[..colon_pos].to_string(), potential_tag.to_string())
} else {
(image.to_string(), "latest".to_string())
}
} else {
(image.to_string(), "latest".to_string())
};
let mut candidates: Vec<(String, String)> = Vec::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut push = |name: String| {
if seen.insert(name.clone()) {
candidates.push((name, reference.clone()));
}
};
push(primary.clone());
if let Some(rest) = primary.strip_prefix("docker.io/") {
push(rest.to_string());
}
if let Some(rest) = primary.strip_prefix("docker.io/library/") {
push(rest.to_string());
}
if let Some(rest) = primary.strip_prefix("library/") {
push(rest.to_string());
}
if !primary.contains('/') {
push(format!("library/{primary}"));
}
if let Some(last) = primary.rsplit('/').next() {
if !last.is_empty() {
push(last.to_string());
}
}
candidates
}
impl std::str::FromStr for ImageRef {
type Err = <ImageReference as std::str::FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = ImageReference::from_str(s)?;
Ok(Self {
parsed,
original: s.to_string(),
})
}
}
impl std::ops::Deref for ImageRef {
type Target = ImageReference;
fn deref(&self) -> &Self::Target {
&self.parsed
}
}
impl std::fmt::Display for ImageRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.original)
}
}
impl PartialEq for ImageRef {
fn eq(&self, other: &Self) -> bool {
self.parsed == other.parsed
}
}
impl Eq for ImageRef {}
impl std::hash::Hash for ImageRef {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.parsed.to_string().hash(state);
}
}
impl serde::Serialize for ImageRef {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.original)
}
}
impl<'de> serde::Deserialize<'de> for ImageRef {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use std::str::FromStr;
let s = String::deserialize(d)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
pub mod api;
pub mod auth;
pub mod builder;
pub mod client;
pub mod cluster;
pub mod jwt;
pub mod overlay;
pub mod overlayd;
pub mod scratch;
pub mod secrets;
pub mod spec;
pub mod storage;
pub use scratch::{Scratch, ScratchFile};
#[cfg(test)]
mod image_ref_tests {
use super::ImageRef;
use std::str::FromStr;
fn parse(s: &str) -> ImageRef {
ImageRef::from_str(s).unwrap_or_else(|e| panic!("failed to parse {s:?}: {e}"))
}
#[test]
fn unqualified_bare_name() {
assert!(parse("nginx").is_unqualified());
}
#[test]
fn unqualified_bare_name_with_tag() {
assert!(parse("nginx:latest").is_unqualified());
}
#[test]
fn unqualified_library_namespace() {
assert!(parse("library/nginx").is_unqualified());
}
#[test]
fn unqualified_user_namespace_with_tag() {
assert!(parse("foo/bar:1.0").is_unqualified());
}
#[test]
fn qualified_docker_io() {
assert!(!parse("docker.io/library/nginx").is_unqualified());
}
#[test]
fn qualified_ghcr() {
assert!(!parse("ghcr.io/foo/bar:1.2").is_unqualified());
}
#[test]
fn qualified_localhost() {
assert!(!parse("localhost/foo").is_unqualified());
}
#[test]
fn qualified_localhost_port() {
assert!(!parse("localhost:5000/foo").is_unqualified());
}
#[test]
fn qualified_registry_port() {
assert!(!parse("registry:5000/foo").is_unqualified());
}
#[test]
fn unqualified_with_digest_tail() {
let s = "foo@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
assert!(parse(s).is_unqualified());
}
#[test]
fn display_preserves_user_input() {
let r = ImageRef::from_str("nginx:latest").unwrap();
assert_eq!(r.to_string(), "nginx:latest");
}
#[test]
fn serde_json_roundtrip_preserves_original() {
let r = ImageRef::from_str("zarcrunner-executor:latest").unwrap();
let json = serde_json::to_string(&r).unwrap();
assert_eq!(json, "\"zarcrunner-executor:latest\"");
let back: ImageRef = serde_json::from_str(&json).unwrap();
assert_eq!(back.original(), "zarcrunner-executor:latest");
}
#[test]
fn equality_uses_canonical_form() {
let bare = ImageRef::from_str("nginx:latest").unwrap();
let qualified = ImageRef::from_str("docker.io/library/nginx:latest").unwrap();
assert_eq!(bare, qualified);
}
}