1use anyhow::Result;
2use std::fmt;
3use url::Url;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct ImageName {
7 pub hostname: String,
8 pub port: Option<u16>,
9 pub name: String,
10 pub reference: Option<String>,
11 pub digest: Option<String>,
12}
13
14impl fmt::Display for ImageName {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 let mut suffix = String::new();
17
18 if let Some(ref reference) = self.reference {
19 suffix.push(':');
20 suffix.push_str(reference);
21 }
22
23 if let Some(ref digest) = self.digest {
24 suffix.push('@');
25 suffix.push_str(digest);
26 }
27
28 if ImageName::DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() {
29 if self.name.starts_with("library/") {
30 write!(f, "{}{}", &self.name[8..], suffix)
31 } else {
32 write!(f, "{}{}", self.name, suffix)
33 }
34 } else if let Some(port) = self.port {
35 write!(f, "{}:{}/{}{}", self.hostname, port, self.name, suffix)
36 } else {
37 write!(f, "{}/{}{}", self.hostname, self.name, suffix)
38 }
39 }
40}
41
42impl Default for ImageName {
43 fn default() -> Self {
44 Self::parse(&format!("{}", uuid::Uuid::new_v4().as_hyphenated()))
45 .expect("UUID hyphenated must be valid name")
46 }
47}
48
49impl ImageName {
50 pub const DOCKER_HUB_MIRROR: &'static str = "mirror.gcr.io";
51 pub const DEFAULT_IMAGE_TAG: &'static str = "latest";
52
53 pub fn parse(name: &str) -> Result<Self> {
54 let full_name = name.to_string();
55 let name = full_name.clone();
56 let (mut hostname, mut name) = name
57 .split_once('/')
58 .map(|x| (x.0.to_string(), x.1.to_string()))
59 .unwrap_or_else(|| {
60 (
61 ImageName::DOCKER_HUB_MIRROR.to_string(),
62 format!("library/{}", name),
63 )
64 });
65
66 if !hostname.contains('.') && full_name.chars().filter(|x| *x == '/').count() == 1 {
71 name = format!("{}/{}", hostname, name);
72 hostname = ImageName::DOCKER_HUB_MIRROR.to_string();
73 }
74
75 let (hostname, port) = if let Some((hostname, port)) = hostname
76 .split_once(':')
77 .map(|x| (x.0.to_string(), x.1.to_string()))
78 {
79 (hostname, Some(str::parse(&port)?))
80 } else {
81 (hostname, None)
82 };
83
84 let name_has_digest = if name.contains('@') {
85 let digest_start = name.chars().position(|c| c == '@');
86 let ref_start = name.chars().position(|c| c == ':');
87 if let (Some(digest_start), Some(ref_start)) = (digest_start, ref_start) {
88 digest_start < ref_start
89 } else {
90 true
91 }
92 } else {
93 false
94 };
95
96 let (name, digest) = if name_has_digest {
97 name.split_once('@')
98 .map(|(name, digest)| (name.to_string(), Some(digest.to_string())))
99 .unwrap_or_else(|| (name, None))
100 } else {
101 (name, None)
102 };
103
104 let (name, reference) = if name.contains(':') {
105 name.split_once(':')
106 .map(|(name, reference)| (name.to_string(), Some(reference.to_string())))
107 .unwrap_or((name, None))
108 } else {
109 (name, None)
110 };
111
112 let (reference, digest) = if let Some(reference) = reference {
113 if let Some(digest) = digest {
114 (Some(reference), Some(digest))
115 } else {
116 reference
117 .split_once('@')
118 .map(|(reff, digest)| (Some(reff.to_string()), Some(digest.to_string())))
119 .unwrap_or_else(|| (Some(reference), None))
120 }
121 } else {
122 (None, digest)
123 };
124
125 Ok(ImageName {
126 hostname,
127 port,
128 name,
129 reference,
130 digest,
131 })
132 }
133
134 pub fn registry_url(&self) -> Result<Url> {
135 let hostname = if let Some(port) = self.port {
136 format!("{}:{}", self.hostname, port)
137 } else {
138 self.hostname.clone()
139 };
140 let url = if self.hostname.starts_with("localhost") {
141 format!("http://{}", hostname)
142 } else {
143 format!("https://{}", hostname)
144 };
145 Ok(Url::parse(&url)?)
146 }
147}