1use crate::ModelError;
4
5#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct ImageReference {
11 authored: String,
12 repository: String,
13 tag: Option<String>,
14 digest: Option<String>,
15}
16
17impl ImageReference {
18 pub fn parse(authored: impl Into<String>) -> Result<Self, ModelError> {
25 let authored = authored.into();
26 validate_component("image reference", &authored)?;
27
28 let mut digest_parts = authored.split('@');
29 let name_and_tag = digest_parts.next().unwrap_or_default();
30 let digest = digest_parts.next();
31 if digest_parts.next().is_some() {
32 return Err(ModelError::InvalidImageReference(
33 "multiple `@` digest separators are not supported",
34 ));
35 }
36 if let Some(value) = digest {
37 validate_component("image digest", value)?;
38 }
39
40 let last_slash = name_and_tag.rfind('/');
41 let last_colon = name_and_tag.rfind(':');
42 let has_tag = last_colon.is_some_and(|colon| last_slash.is_none_or(|slash| colon > slash));
43 let (repository, tag) = if has_tag {
44 let colon = last_colon.unwrap_or_default();
45 (&name_and_tag[..colon], Some(&name_and_tag[colon + 1..]))
46 } else {
47 (name_and_tag, None)
48 };
49 validate_component("image repository", repository)?;
50 if let Some(value) = tag {
51 validate_component("image tag", value)?;
52 }
53
54 let repository = repository.to_owned();
55 let tag = tag.map(str::to_owned);
56 let digest = digest.map(str::to_owned);
57 Ok(Self {
58 authored,
59 repository,
60 tag,
61 digest,
62 })
63 }
64
65 #[must_use]
67 pub fn as_str(&self) -> &str {
68 &self.authored
69 }
70
71 #[must_use]
73 pub fn repository(&self) -> &str {
74 &self.repository
75 }
76
77 #[must_use]
79 pub fn tag(&self) -> Option<&str> {
80 self.tag.as_deref()
81 }
82
83 #[must_use]
85 pub fn digest(&self) -> Option<&str> {
86 self.digest.as_deref()
87 }
88}
89
90fn validate_component(kind: &'static str, value: &str) -> Result<(), ModelError> {
91 if value.is_empty() {
92 return Err(ModelError::EmptyValue(kind));
93 }
94 if value.contains('\0') {
95 return Err(ModelError::ContainsNul(kind));
96 }
97 Ok(())
98}
99
100#[cfg(test)]
101mod tests {
102 use super::ImageReference;
103
104 #[test]
105 fn accepts_registry_port_tag_and_digest_together() -> Result<(), String> {
106 let image = ImageReference::parse("registry.example:5000/team/app:1.2@sha256:abcd")
107 .map_err(|error| error.to_string())?;
108 assert_eq!(image.repository(), "registry.example:5000/team/app");
109 assert_eq!(image.tag(), Some("1.2"));
110 assert_eq!(image.digest(), Some("sha256:abcd"));
111 assert_eq!(image.as_str(), "registry.example:5000/team/app:1.2@sha256:abcd");
112 Ok(())
113 }
114
115 #[test]
116 fn does_not_treat_a_registry_port_as_a_tag() -> Result<(), String> {
117 let image =
118 ImageReference::parse("registry.example:5000/team/app@sha256:abcd").map_err(|error| error.to_string())?;
119 assert_eq!(image.repository(), "registry.example:5000/team/app");
120 assert_eq!(image.tag(), None);
121 Ok(())
122 }
123}