1use std::convert::TryFrom;
2use std::error::Error;
3use std::fmt;
4use std::str::FromStr;
5
6use crate::regexp;
7
8const NAME_TOTAL_LENGTH_MAX: usize = 255;
10
11const DOCKER_HUB_DOMAIN_LEGACY: &str = "index.docker.io";
12const DOCKER_HUB_DOMAIN: &str = "docker.io";
13const DOCKER_HUB_OFFICIAL_REPO_NAME: &str = "library";
14const DEFAULT_TAG: &str = "latest";
15
16#[derive(Debug, PartialEq, Eq)]
18pub enum ParseError {
19 DigestInvalidFormat,
21 DigestInvalidLength,
23 DigestUnsupported,
25 NameContainsUppercase,
27 NameEmpty,
29 NameTooLong,
31 ReferenceInvalidFormat,
33 TagInvalidFormat,
35}
36
37impl fmt::Display for ParseError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 ParseError::DigestInvalidFormat => write!(f, "invalid checksum digest format"),
41 ParseError::DigestInvalidLength => write!(f, "invalid checksum digest length"),
42 ParseError::DigestUnsupported => write!(f, "unsupported digest algorithm"),
43 ParseError::NameContainsUppercase => write!(f, "repository name must be lowercase"),
44 ParseError::NameEmpty => write!(f, "repository name must have at least one component"),
45 ParseError::NameTooLong => write!(
46 f,
47 "repository name must not be more than {} characters",
48 NAME_TOTAL_LENGTH_MAX
49 ),
50 ParseError::ReferenceInvalidFormat => write!(f, "invalid reference format"),
51 ParseError::TagInvalidFormat => write!(f, "invalid tag format"),
52 }
53 }
54}
55
56impl Error for ParseError {}
57
58#[derive(Clone, Hash, PartialEq, Eq, Debug)]
76pub struct Reference {
77 registry: String,
78 repository: String,
79 tag: Option<String>,
80 digest: Option<String>,
81}
82
83impl Reference {
84 pub fn with_tag(registry: String, repository: String, tag: String) -> Self {
86 Self {
87 registry,
88 repository,
89 tag: Some(tag),
90 digest: None,
91 }
92 }
93
94 pub fn with_digest(registry: String, repository: String, digest: String) -> Self {
96 Self {
97 registry,
98 repository,
99 tag: None,
100 digest: Some(digest),
101 }
102 }
103
104 pub fn resolve_registry(&self) -> &str {
109 let registry = self.registry();
110 match registry {
111 "docker.io" => "index.docker.io",
112 _ => registry,
113 }
114 }
115
116 pub fn registry(&self) -> &str {
118 &self.registry
119 }
120
121 pub fn repository(&self) -> &str {
123 &self.repository
124 }
125
126 pub fn tag(&self) -> Option<&str> {
128 self.tag.as_deref()
129 }
130
131 pub fn digest(&self) -> Option<&str> {
133 self.digest.as_deref()
134 }
135
136 fn full_name(&self) -> String {
138 if self.registry() == "" {
139 self.repository().to_string()
140 } else {
141 format!("{}/{}", self.registry(), self.repository())
142 }
143 }
144
145 pub fn whole(&self) -> String {
147 let mut s = self.full_name();
148 if let Some(t) = self.tag() {
149 if !s.is_empty() {
150 s.push(':');
151 }
152 s.push_str(t);
153 }
154 if let Some(d) = self.digest() {
155 if !s.is_empty() {
156 s.push('@');
157 }
158 s.push_str(d);
159 }
160 s
161 }
162}
163
164impl fmt::Display for Reference {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 write!(f, "{}", self.whole())
167 }
168}
169
170impl FromStr for Reference {
171 type Err = ParseError;
172
173 fn from_str(s: &str) -> Result<Self, Self::Err> {
174 Reference::try_from(s)
175 }
176}
177
178impl TryFrom<String> for Reference {
179 type Error = ParseError;
180
181 fn try_from(s: String) -> Result<Self, Self::Error> {
182 if s.is_empty() {
183 return Err(ParseError::NameEmpty);
184 }
185 lazy_static! {
186 static ref RE: regex::Regex = regexp::must_compile(regexp::REFERENCE_REGEXP);
187 };
188 let captures = match RE.captures(&s) {
189 Some(caps) => caps,
190 None => {
191 return Err(ParseError::ReferenceInvalidFormat);
192 }
193 };
194 let name = &captures[1];
195 let mut tag = captures.get(2).map(|m| m.as_str().to_owned());
196 let digest = captures.get(3).map(|m| m.as_str().to_owned());
197 if tag.is_none() && digest.is_none() {
198 tag = Some(DEFAULT_TAG.into());
199 }
200 let (registry, repository) = split_domain(name);
201 let reference = Reference {
202 registry,
203 repository,
204 tag,
205 digest,
206 };
207 if reference.repository().len() > NAME_TOTAL_LENGTH_MAX {
208 return Err(ParseError::NameTooLong);
209 }
210 if reference.digest().is_some() {
213 let d = reference.digest().unwrap();
214 if d.len() < 8 {
218 return Err(ParseError::DigestInvalidFormat);
219 }
220 let algo = &d[0..6];
221 let digest = &d[7..];
222 match algo {
223 "sha256" => {
224 if digest.len() != 64 {
225 return Err(ParseError::DigestInvalidLength);
226 }
227 }
228 "sha384" => {
229 if digest.len() != 96 {
230 return Err(ParseError::DigestInvalidLength);
231 }
232 }
233 "sha512" => {
234 if digest.len() != 128 {
235 return Err(ParseError::DigestInvalidLength);
236 }
237 }
238 _ => return Err(ParseError::DigestUnsupported),
239 }
240 }
241 Ok(reference)
242 }
243}
244
245impl TryFrom<&str> for Reference {
246 type Error = ParseError;
247 fn try_from(string: &str) -> Result<Self, Self::Error> {
248 TryFrom::try_from(string.to_owned())
249 }
250}
251
252impl From<Reference> for String {
253 fn from(reference: Reference) -> Self {
254 reference.whole()
255 }
256}
257
258fn split_domain(name: &str) -> (String, String) {
265 let mut domain: String;
266 let mut remainder: String;
267
268 match name.split_once('/') {
269 None => {
270 domain = DOCKER_HUB_DOMAIN.into();
271 remainder = name.into();
272 }
273 Some((left, right)) => {
274 if !(left.contains('.') || left.contains(':')) && left != "localhost" {
275 domain = DOCKER_HUB_DOMAIN.into();
276 remainder = name.into();
277 } else {
278 domain = left.into();
279 remainder = right.into();
280 }
281 }
282 }
283 if domain == DOCKER_HUB_DOMAIN_LEGACY {
284 domain = DOCKER_HUB_DOMAIN.into();
285 }
286 if domain == DOCKER_HUB_DOMAIN && !remainder.contains('/') {
287 remainder = format!("{}/{}", DOCKER_HUB_OFFICIAL_REPO_NAME, remainder);
288 }
289
290 (domain, remainder)
291}
292
293#[cfg(test)]
294mod test {
295 use super::*;
296
297 mod parse {
298 use super::*;
299 use rstest::rstest;
300
301 #[rstest(input, registry, repository, tag, digest, whole,
302 case("busybox", "docker.io", "library/busybox", Some("latest"), None, "docker.io/library/busybox:latest"),
303 case("test.com:tag", "docker.io", "library/test.com", Some("tag"), None, "docker.io/library/test.com:tag"),
304 case("test.com:5000", "docker.io", "library/test.com", Some("5000"), None, "docker.io/library/test.com:5000"),
305 case("test.com/repo:tag", "test.com", "repo", Some("tag"), None, "test.com/repo:tag"),
306 case("test:5000/repo", "test:5000", "repo", Some("latest"), None, "test:5000/repo:latest"),
307 case("test:5000/repo:tag", "test:5000", "repo", Some("tag"), None, "test:5000/repo:tag"),
308 case("test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "test:5000", "repo", None, Some("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
309 case("test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "test:5000", "repo", Some("tag"), Some("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
310 case("lowercase:Uppercase", "docker.io", "library/lowercase", Some("Uppercase"), None, "docker.io/library/lowercase:Uppercase"),
311 case("sub-dom1.foo.com/bar/baz/quux", "sub-dom1.foo.com", "bar/baz/quux", Some("latest"), None, "sub-dom1.foo.com/bar/baz/quux:latest"),
312 case("sub-dom1.foo.com/bar/baz/quux:some-long-tag", "sub-dom1.foo.com", "bar/baz/quux", Some("some-long-tag"), None, "sub-dom1.foo.com/bar/baz/quux:some-long-tag"),
313 case("b.gcr.io/test.example.com/my-app:test.example.com", "b.gcr.io", "test.example.com/my-app", Some("test.example.com"), None, "b.gcr.io/test.example.com/my-app:test.example.com"),
314 case("xn--n3h.com/myimage:xn--n3h.com", "xn--n3h.com", "myimage", Some("xn--n3h.com"), None, "xn--n3h.com/myimage:xn--n3h.com"),
316 case("xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "xn--7o8h.com", "myimage", Some("xn--7o8h.com"), Some("sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
318 case("foo_bar.com:8080", "docker.io", "library/foo_bar.com", Some("8080"), None, "docker.io/library/foo_bar.com:8080" ),
319 case("foo/foo_bar.com:8080", "docker.io", "foo/foo_bar.com", Some("8080"), None, "docker.io/foo/foo_bar.com:8080"),
320 case("opensuse/leap:15.3", "docker.io", "opensuse/leap", Some("15.3"), None, "docker.io/opensuse/leap:15.3"),
321 )]
322 fn parse_good_reference(
323 input: &str,
324 registry: &str,
325 repository: &str,
326 tag: Option<&str>,
327 digest: Option<&str>,
328 whole: &str,
329 ) {
330 println!("input: {}", input);
331 let reference = Reference::try_from(input).expect("could not parse reference");
332 println!("{} -> {:?}", input, reference);
333 assert_eq!(registry, reference.registry());
334 assert_eq!(repository, reference.repository());
335 assert_eq!(tag, reference.tag());
336 assert_eq!(digest, reference.digest());
337 assert_eq!(whole, reference.whole());
338 }
339
340 #[rstest(input, err,
341 case("", ParseError::NameEmpty),
342 case(":justtag", ParseError::ReferenceInvalidFormat),
343 case("@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ParseError::ReferenceInvalidFormat),
344 case("repo@sha256:ffffffffffffffffffffffffffffffffff", ParseError::DigestInvalidLength),
345 case("validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ParseError::DigestUnsupported),
346 case("Uppercase:tag", ParseError::ReferenceInvalidFormat),
348 case("test:5000/Uppercase/lowercase:tag", ParseError::ReferenceInvalidFormat),
353 case("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ParseError::NameTooLong),
354 case("aa/asdf$$^/aa", ParseError::ReferenceInvalidFormat)
355 )]
356 fn parse_bad_reference(input: &str, err: ParseError) {
357 assert_eq!(Reference::try_from(input).unwrap_err(), err)
358 }
359 }
360}