docker_registry/
reference.rs1use std::{collections::VecDeque, fmt, str, str::FromStr};
31
32use regex_lite::Regex;
33
34pub static DEFAULT_REGISTRY: &str = "registry-1.docker.io";
35static DEFAULT_TAG: &str = "latest";
36static DEFAULT_SCHEME: &str = "docker";
37
38#[derive(Clone)]
40pub enum Version {
41 Tag(String),
42 Digest(String, String),
43}
44
45#[derive(thiserror::Error, Debug)]
46pub enum VersionParseError {
47 #[error("wrong digest format: checksum missing")]
48 WrongDigestFormat,
49 #[error("unknown prefix: digest must start from : or @")]
50 UnknownPrefix,
51 #[error("empty string is invalid digest")]
52 Empty,
53}
54
55impl str::FromStr for Version {
56 type Err = VersionParseError;
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 let v = match s.chars().next() {
59 Some(':') => Version::Tag(s.trim_start_matches(':').to_string()),
60 Some('@') => {
61 let r: Vec<&str> = s.trim_start_matches('@').splitn(2, ':').collect();
62 if r.len() != 2 {
63 return Err(VersionParseError::WrongDigestFormat);
64 };
65 Version::Digest(r[0].to_string(), r[1].to_string())
66 }
67 Some(_) => return Err(VersionParseError::UnknownPrefix),
68 None => return Err(VersionParseError::Empty),
69 };
70 Ok(v)
71 }
72}
73
74impl Default for Version {
75 fn default() -> Self {
76 Version::Tag("latest".to_string())
77 }
78}
79
80impl fmt::Debug for Version {
81 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
82 let v = match *self {
83 Version::Tag(ref s) => ":".to_string() + s,
84 Version::Digest(ref t, ref d) => "@".to_string() + t + ":" + d,
85 };
86 write!(f, "{v}")
87 }
88}
89
90impl fmt::Display for Version {
91 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
92 let v = match *self {
93 Version::Tag(ref s) => s.to_string(),
94 Version::Digest(ref t, ref d) => t.to_string() + ":" + d,
95 };
96 write!(f, "{v}")
97 }
98}
99
100#[derive(Clone, Debug, Default)]
102pub struct Reference {
103 raw_input: String,
104 registry: String,
105 repository: String,
106 version: Version,
107}
108
109impl Reference {
110 pub fn new(registry: Option<String>, repository: String, version: Option<Version>) -> Self {
111 let reg = registry.unwrap_or_else(|| DEFAULT_REGISTRY.to_string());
112 let ver = version.unwrap_or_else(|| Version::Tag(DEFAULT_TAG.to_string()));
113 Self {
114 raw_input: "".into(),
115 registry: reg,
116 repository,
117 version: ver,
118 }
119 }
120
121 pub fn registry(&self) -> String {
122 self.registry.clone()
123 }
124
125 pub fn repository(&self) -> String {
126 self.repository.clone()
127 }
128
129 pub fn version(&self) -> String {
130 self.version.to_string()
131 }
132
133 pub fn to_raw_string(&self) -> String {
134 self.raw_input.clone()
135 }
136
137 pub fn to_url(&self) -> String {
139 format!(
140 "{}://{}/{}{:?}",
141 DEFAULT_SCHEME, self.registry, self.repository, self.version
142 )
143 }
144}
145
146impl fmt::Display for Reference {
147 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
148 write!(f, "{}/{}{:?}", self.registry, self.repository, self.version)
149 }
150}
151
152impl str::FromStr for Reference {
153 type Err = ReferenceParseError;
154 fn from_str(s: &str) -> Result<Self, Self::Err> {
155 parse_url(s)
156 }
157}
158
159#[derive(thiserror::Error, Debug)]
160pub enum ReferenceParseError {
161 #[error("missing image name")]
162 MissingImageName,
163 #[error("version parse error")]
164 VersionParse(#[from] VersionParseError),
165 #[error("empty image name")]
166 EmptyImageName,
167 #[error("component '{component}' does not conform to regex '{regex}'")]
168 RegexViolation { regex: &'static str, component: String },
169 #[error("empty repository name")]
170 EmptyRepositoryName,
171 #[error("repository name too long")]
172 RepositoryNameTooLong,
173}
174
175fn parse_url(input: &str) -> Result<Reference, ReferenceParseError> {
176 let mut rest = input;
178
179 let has_schema = rest.starts_with("docker://");
181 if has_schema {
182 rest = input.trim_start_matches("docker://");
183 };
184
185 let mut components: VecDeque<String> = rest.split('/').filter(|s| !s.is_empty()).map(String::from).collect();
187
188 let first = components.pop_front().ok_or(ReferenceParseError::MissingImageName)?;
191
192 let registry = if Regex::new(
193 r"(?x)
194 ^
195 # hostname
196 (([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])
197
198 # optional port
199 ([:][0-9]{1,6})?
200 $
201 ",
202 )
203 .expect("hardcoded regex is invalid")
204 .is_match(&first)
205 {
206 first
207 } else {
208 components.push_front(first);
209 DEFAULT_REGISTRY.to_string()
210 };
211
212 let last = components.pop_back().ok_or(ReferenceParseError::MissingImageName)?;
214 let (image_name, version) = match (last.rfind('@'), last.rfind(':')) {
215 (Some(i), _) | (None, Some(i)) => {
216 let s = last.split_at(i);
217 (String::from(s.0), Version::from_str(s.1)?)
218 }
219 (None, None) => (last, Version::default()),
220 };
221 if image_name.is_empty() {
222 return Err(ReferenceParseError::EmptyImageName);
223 }
224
225 if components.is_empty() && registry == DEFAULT_REGISTRY {
228 components.push_back("library".to_string());
229 }
230 components.push_back(image_name);
231
232 const REGEX: &str = "^[a-z0-9]+(?:[._-][a-z0-9]+)*$";
235 let path_re = Regex::new(REGEX).expect("hardcoded regex is invalid");
236 components.iter().try_for_each(|component| {
237 if !path_re.is_match(component) {
238 return Err(ReferenceParseError::RegexViolation {
239 component: component.clone(),
240 regex: REGEX,
241 });
242 };
243
244 Ok(())
245 })?;
246
247 let repository = components.into_iter().collect::<Vec<_>>().join("/");
249 if repository.is_empty() {
250 return Err(ReferenceParseError::EmptyRepositoryName);
251 }
252 if repository.len() > 127 {
253 return Err(ReferenceParseError::RepositoryNameTooLong);
254 }
255
256 Ok(Reference {
257 raw_input: input.to_string(),
258 registry,
259 repository,
260 version,
261 })
262}