Skip to main content

oci_image/
reference.rs

1use crate::{Error, Result};
2
3/// A parsed image reference such as `busybox`, `nginx:1.27` or
4/// `ghcr.io/org/app@sha256:...`, with Docker's defaults filled in.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct ImageRef {
7    pub registry: String,
8    pub repository: String,
9    /// A tag, or a digest (`sha256:...`).
10    pub reference: String,
11}
12
13impl ImageRef {
14    pub fn parse(image: &str) -> Result<Self> {
15        let image = image.trim();
16        if image.is_empty() {
17            return Err(Error::BadReference(image.to_string()));
18        }
19        let (name, digest) = match image.split_once('@') {
20            Some((n, d)) => (n, Some(d)),
21            None => (image, None),
22        };
23        let (registry, rest) = match name.split_once('/') {
24            Some((first, rest))
25                if first.contains('.') || first.contains(':') || first == "localhost" =>
26            {
27                (first.to_string(), rest.to_string())
28            }
29            _ => ("docker.io".to_string(), name.to_string()),
30        };
31        // A tag is a ':' after the last '/'.
32        let (repository, tag) = match rest.rfind(':') {
33            Some(i) if !rest[i..].contains('/') => {
34                (rest[..i].to_string(), rest[i + 1..].to_string())
35            }
36            _ => (rest, "latest".to_string()),
37        };
38        let repository = if registry == "docker.io" && !repository.contains('/') {
39            format!("library/{repository}")
40        } else {
41            repository
42        };
43        if repository.is_empty() || repository.ends_with('/') {
44            return Err(Error::BadReference(image.to_string()));
45        }
46        Ok(Self {
47            registry,
48            repository,
49            reference: digest.map(str::to_string).unwrap_or(tag),
50        })
51    }
52
53    /// The fully qualified name, e.g. `docker.io/library/busybox:latest`.
54    pub fn canonical(&self) -> String {
55        if self.reference.contains(':') {
56            format!("{}/{}@{}", self.registry, self.repository, self.reference)
57        } else {
58            format!("{}/{}:{}", self.registry, self.repository, self.reference)
59        }
60    }
61
62    /// The registry API base for this repository, `https://<host>/v2/<repo>`
63    /// (`http` for localhost).
64    pub fn api_base(&self) -> String {
65        let host = if self.registry == "docker.io" {
66            "registry-1.docker.io"
67        } else {
68            &self.registry
69        };
70        let scheme = if host.starts_with("localhost") || host.starts_with("127.0.0.1") {
71            "http"
72        } else {
73            "https"
74        };
75        format!("{scheme}://{host}/v2/{}", self.repository)
76    }
77}