mod archive;
mod blob_source;
mod config;
mod image_disk;
mod manager;
mod object;
mod storage;
mod store;
pub use archive::LayerExtractor;
pub use config::ContainerImageConfig;
pub use image_disk::ImageDiskManager;
pub use manager::ImageManager;
pub use object::ImageObject;
use oci_client::Reference;
pub(crate) struct ReferenceIter<'a> {
base_ref: Reference,
registries: &'a [String],
index: usize,
is_qualified: bool,
yielded_original: bool,
}
impl<'a> ReferenceIter<'a> {
pub fn new(image_ref: &str, registries: &'a [String]) -> Result<Self, oci_client::ParseError> {
let base_ref: Reference = image_ref.parse()?;
let is_qualified = is_fully_qualified(image_ref);
tracing::debug!(
image_ref = %image_ref,
is_qualified = %is_qualified,
registry_count = registries.len(),
"Created reference iterator for image resolution"
);
Ok(Self {
base_ref,
registries,
index: 0,
is_qualified,
yielded_original: false,
})
}
}
impl Iterator for ReferenceIter<'_> {
type Item = Reference;
fn next(&mut self) -> Option<Self::Item> {
if self.is_qualified {
if self.yielded_original {
return None;
}
self.yielded_original = true;
return Some(self.base_ref.clone());
}
if self.registries.is_empty() {
if self.yielded_original {
return None;
}
self.yielded_original = true;
return Some(self.base_ref.clone());
}
if self.index >= self.registries.len() {
return None;
}
let registry = &self.registries[self.index];
self.index += 1;
let tag = self.base_ref.tag().unwrap_or("latest").to_string();
Some(Reference::with_tag(
registry.clone(),
self.base_ref.repository().to_string(),
tag,
))
}
}
fn is_fully_qualified(image_ref: &str) -> bool {
if let Some(slash_pos) = image_ref.find('/') {
let first_part = &image_ref[..slash_pos];
first_part.contains('.') || first_part.contains(':') || first_part == "localhost"
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn collect_refs(iter: ReferenceIter) -> Vec<(String, String, Option<String>)> {
iter.map(|r| {
(
r.registry().to_string(),
r.repository().to_string(),
r.tag().map(|t| t.to_string()),
)
})
.collect()
}
#[test]
fn test_empty_registries_yields_original() {
let registries: Vec<String> = vec![];
let iter = ReferenceIter::new("alpine", ®istries).unwrap();
let refs = collect_refs(iter);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].0, "docker.io");
}
#[test]
fn test_single_registry() {
let registries = vec!["ghcr.io".to_string()];
let iter = ReferenceIter::new("alpine", ®istries).unwrap();
let refs = collect_refs(iter);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].0, "ghcr.io");
}
#[test]
fn test_multiple_registries() {
let registries = vec![
"ghcr.io".to_string(),
"quay.io".to_string(),
"docker.io".to_string(),
];
let iter = ReferenceIter::new("alpine:3.18", ®istries).unwrap();
let refs = collect_refs(iter);
assert_eq!(refs.len(), 3);
assert_eq!(refs[0].0, "ghcr.io");
assert_eq!(refs[1].0, "quay.io");
assert_eq!(refs[2].0, "docker.io");
for r in &refs {
assert_eq!(r.2, Some("3.18".to_string()));
}
}
#[test]
fn test_qualified_bypasses_registries() {
let registries = vec!["ghcr.io".to_string()];
let iter = ReferenceIter::new("docker.io/library/alpine", ®istries).unwrap();
let refs = collect_refs(iter);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].0, "docker.io");
let iter = ReferenceIter::new("quay.io/foo/bar:v1", ®istries).unwrap();
let refs = collect_refs(iter);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].0, "quay.io");
let iter = ReferenceIter::new("localhost/myimage", ®istries).unwrap();
let refs = collect_refs(iter);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].0, "localhost");
}
#[test]
fn test_namespace_not_registry() {
let registries = vec!["ghcr.io".to_string()];
let iter = ReferenceIter::new("library/alpine", ®istries).unwrap();
let refs = collect_refs(iter);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].0, "ghcr.io");
assert!(refs[0].1.contains("library"));
}
#[test]
fn test_is_fully_qualified() {
assert!(is_fully_qualified("docker.io/library/alpine"));
assert!(is_fully_qualified("ghcr.io/owner/repo"));
assert!(is_fully_qualified("localhost/myimage"));
assert!(is_fully_qualified("localhost:5000/myimage"));
assert!(is_fully_qualified("my-registry.com:5000/image"));
assert!(!is_fully_qualified("alpine"));
assert!(!is_fully_qualified("alpine:latest"));
assert!(!is_fully_qualified("library/alpine"));
assert!(!is_fully_qualified("myorg/myimage:v1"));
}
}