Skip to main content

hd_oci/
registry.rs

1// OCI registry client — image reference parsing and manifest types.
2
3#[derive(Debug, Clone)]
4pub struct ImageRef {
5    pub registry: String,
6    pub repository: String,
7    pub tag: String,
8}
9
10#[derive(Debug, thiserror::Error)]
11pub enum RegistryError {
12    #[error("invalid image reference: {0}")]
13    InvalidRef(String),
14    #[error("registry error: {0}")]
15    Registry(String),
16    #[error("HTTP error: {0}")]
17    Http(String),
18}
19
20impl ImageRef {
21    pub fn parse(reference: &str) -> Result<Self, RegistryError> {
22        let (name, tag) = if let Some((n, t)) = reference.rsplit_once(':') {
23            // Check if the colon is part of a port (e.g., localhost:5000/image)
24            if n.contains('/') || !t.contains('/') {
25                (n.to_string(), t.to_string())
26            } else {
27                (reference.to_string(), "latest".to_string())
28            }
29        } else {
30            (reference.to_string(), "latest".to_string())
31        };
32
33        let (registry, repository) =
34            if name.contains('.') || name.contains(':') || name.contains("localhost") {
35                // Has a registry prefix
36                if let Some((reg, repo)) = name.split_once('/') {
37                    (reg.to_string(), repo.to_string())
38                } else {
39                    (name.clone(), name)
40                }
41            } else if name.contains('/') {
42                // Docker Hub with org
43                ("registry-1.docker.io".to_string(), name)
44            } else {
45                // Docker Hub official image
46                (
47                    "registry-1.docker.io".to_string(),
48                    format!("library/{}", name),
49                )
50            };
51
52        Ok(ImageRef {
53            registry,
54            repository,
55            tag,
56        })
57    }
58}
59
60impl std::fmt::Display for ImageRef {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(f, "{}/{}:{}", self.registry, self.repository, self.tag)
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn parse_simple_reference() {
72        let r = ImageRef::parse("ubuntu:22.04").unwrap();
73        assert_eq!(r.registry, "registry-1.docker.io");
74        assert_eq!(r.repository, "library/ubuntu");
75        assert_eq!(r.tag, "22.04");
76    }
77
78    #[test]
79    fn parse_full_reference() {
80        let r = ImageRef::parse("ghcr.io/owner/repo:v1.0").unwrap();
81        assert_eq!(r.registry, "ghcr.io");
82        assert_eq!(r.repository, "owner/repo");
83        assert_eq!(r.tag, "v1.0");
84    }
85
86    #[test]
87    fn parse_no_tag_defaults_latest() {
88        let r = ImageRef::parse("alpine").unwrap();
89        assert_eq!(r.tag, "latest");
90    }
91
92    #[test]
93    fn parse_with_port() {
94        let r = ImageRef::parse("localhost:5000/myimage:dev").unwrap();
95        assert_eq!(r.registry, "localhost:5000");
96        assert_eq!(r.repository, "myimage");
97        assert_eq!(r.tag, "dev");
98    }
99}