1use crate::{Error, Result};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct ImageRef {
7 pub registry: String,
8 pub repository: String,
9 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 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 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 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}